Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2207dd9c32 | ||
|
|
3204a79eb6 | ||
|
|
e9ac115a95 | ||
|
|
c90c786f39 | ||
|
|
f10e2ac639 | ||
|
|
c2d899f2a3 | ||
|
|
7415d8e361 | ||
|
|
298dcc4790 | ||
|
|
2f2890c093 | ||
|
|
e463aec5f6 | ||
|
|
6b3e11a47c | ||
|
|
0985e3bed8 | ||
|
|
6d8fd267de | ||
|
|
e89bb932ea | ||
|
|
77737dbab6 | ||
|
|
271c3dc91d | ||
|
|
5860a27ab5 | ||
|
|
819093cb7e | ||
|
|
855c106e9b | ||
|
|
04de262268 | ||
|
|
26d8870cab | ||
|
|
aec867ae69 | ||
|
|
a515335c15 | ||
|
|
96dd9809f4 | ||
|
|
20b4c86023 | ||
|
|
6f8a728c5f | ||
|
|
1669f91a2d | ||
|
|
3d265afa37 | ||
|
|
aa050e9292 | ||
|
|
91a9fa5274 | ||
|
|
150e33a1c7 | ||
|
|
da9a6282c7 | ||
|
|
907b611505 | ||
|
|
eaf205eded | ||
|
|
329bd86e09 | ||
|
|
19bb831fbf | ||
|
|
bca38bd372 | ||
|
|
9095649afb |
6
.github/codecov.yml
vendored
Normal file
6
.github/codecov.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
ignore:
|
||||
- "watchdog/watchdog.go"
|
||||
|
||||
coverage:
|
||||
status:
|
||||
patch: off
|
||||
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -15,16 +15,18 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Set up Go 1.15
|
||||
uses: actions/setup-go@v1
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.15
|
||||
id: go
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
- name: Build binary to make sure it works
|
||||
run: go build -mod vendor
|
||||
- name: Test
|
||||
run: sudo go test -mod vendor ./... -race -coverprofile=coverage.txt -covermode=atomic
|
||||
# We're using "sudo" because one of the tests leverages ping, which requires super-user privileges.
|
||||
# As for the "PATH=$PATH", we need it to use the same "go" executable that was configured by the "Set
|
||||
# up Go" step (otherwise, it'd use sudo's "go" executable)
|
||||
run: sudo "PATH=$PATH" go test -mod vendor ./... -race -coverprofile=coverage.txt -covermode=atomic
|
||||
- name: Codecov
|
||||
uses: codecov/codecov-action@v1.0.14
|
||||
with:
|
||||
|
||||
@@ -2,9 +2,11 @@ package custom
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/TwinProduction/gatus/client"
|
||||
@@ -74,6 +76,12 @@ func (provider *AlertProvider) buildHTTPRequest(serviceName, alertDescription st
|
||||
|
||||
// Send a request to the alert provider and return the body
|
||||
func (provider *AlertProvider) Send(serviceName, alertDescription string, resolved bool) ([]byte, error) {
|
||||
if os.Getenv("MOCK_ALERT_PROVIDER") == "true" {
|
||||
if os.Getenv("MOCK_ALERT_PROVIDER_ERROR") == "true" {
|
||||
return nil, errors.New("error")
|
||||
}
|
||||
return []byte("{}"), nil
|
||||
}
|
||||
request := provider.buildHTTPRequest(serviceName, alertDescription, resolved)
|
||||
response, err := client.GetHTTPClient(provider.Insecure).Do(request)
|
||||
if err != nil {
|
||||
|
||||
@@ -31,7 +31,7 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler
|
||||
for _, conditionResult := range result.ConditionResults {
|
||||
var prefix string
|
||||
if conditionResult.Success {
|
||||
prefix = ":heavy_check_mark:"
|
||||
prefix = ":white_check_mark:"
|
||||
} else {
|
||||
prefix = ":x:"
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ import (
|
||||
var (
|
||||
secureHTTPClient *http.Client
|
||||
insecureHTTPClient *http.Client
|
||||
|
||||
// pingTimeout is the timeout for the Ping function
|
||||
// This is mainly exposed for testing purposes
|
||||
pingTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
// GetHTTPClient returns the shared HTTP client
|
||||
@@ -19,7 +23,7 @@ func GetHTTPClient(insecure bool) *http.Client {
|
||||
if insecure {
|
||||
if insecureHTTPClient == nil {
|
||||
insecureHTTPClient = &http.Client{
|
||||
Timeout: time.Second * 10,
|
||||
Timeout: 10 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 20,
|
||||
@@ -62,7 +66,7 @@ func Ping(address string) (bool, time.Duration) {
|
||||
return false, 0
|
||||
}
|
||||
pinger.Count = 1
|
||||
pinger.Timeout = 5 * time.Second
|
||||
pinger.Timeout = pingTimeout
|
||||
pinger.SetNetwork("ip4")
|
||||
pinger.SetPrivileged(true)
|
||||
err = pinger.Run()
|
||||
@@ -70,6 +74,10 @@ func Ping(address string) (bool, time.Duration) {
|
||||
return false, 0
|
||||
}
|
||||
if pinger.Statistics() != nil {
|
||||
// If the packet loss is 100, it means that the packet didn't reach the host
|
||||
if pinger.Statistics().PacketLoss == 100 {
|
||||
return false, pinger.Timeout
|
||||
}
|
||||
return true, pinger.Statistics().MaxRtt
|
||||
}
|
||||
return true, 0
|
||||
|
||||
@@ -2,6 +2,7 @@ package client
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGetHTTPClient(t *testing.T) {
|
||||
@@ -28,6 +29,7 @@ func TestGetHTTPClient(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPing(t *testing.T) {
|
||||
pingTimeout = 500 * time.Millisecond
|
||||
if success, rtt := Ping("127.0.0.1"); !success {
|
||||
t.Error("expected true")
|
||||
if rtt == 0 {
|
||||
@@ -35,7 +37,13 @@ func TestPing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
if success, rtt := Ping("256.256.256.256"); success {
|
||||
t.Error("expected false")
|
||||
t.Error("expected false, because the IP is invalid")
|
||||
if rtt != 0 {
|
||||
t.Error("Round-trip time returned on failure should've been 0")
|
||||
}
|
||||
}
|
||||
if success, rtt := Ping("192.168.152.153"); success {
|
||||
t.Error("expected false, because the IP is valid but the host should be unreachable")
|
||||
if rtt != 0 {
|
||||
t.Error("Round-trip time returned on failure should've been 0")
|
||||
}
|
||||
|
||||
@@ -86,6 +86,12 @@ func Get() *Config {
|
||||
return config
|
||||
}
|
||||
|
||||
// Set sets the configuration
|
||||
// Used only for testing
|
||||
func Set(cfg *Config) {
|
||||
config = cfg
|
||||
}
|
||||
|
||||
// Load loads a custom configuration file
|
||||
// Note that the misconfiguration of some fields may lead to panics. This is on purpose.
|
||||
func Load(configFile string) error {
|
||||
|
||||
@@ -17,6 +17,16 @@ func TestGetBeforeConfigIsLoaded(t *testing.T) {
|
||||
t.Fatal("Should've panicked because the configuration hasn't been loaded yet")
|
||||
}
|
||||
|
||||
func TestSet(t *testing.T) {
|
||||
if config != nil {
|
||||
t.Fatal("config should've been nil")
|
||||
}
|
||||
Set(&Config{})
|
||||
if config == nil {
|
||||
t.Fatal("config shouldn't have been nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFileThatDoesNotExist(t *testing.T) {
|
||||
err := Load("file-that-does-not-exist.yaml")
|
||||
if err == nil {
|
||||
@@ -155,7 +165,7 @@ web:
|
||||
port: 12345
|
||||
services:
|
||||
- name: twinnation
|
||||
url: https://twinnation.org/actuator/health
|
||||
url: https://twinnation.org/health
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
`))
|
||||
@@ -168,17 +178,15 @@ services:
|
||||
if config.Metrics {
|
||||
t.Error("Metrics should've been false by default")
|
||||
}
|
||||
if config.Services[0].URL != "https://twinnation.org/actuator/health" {
|
||||
t.Errorf("URL should have been %s", "https://twinnation.org/actuator/health")
|
||||
if config.Services[0].URL != "https://twinnation.org/health" {
|
||||
t.Errorf("URL should have been %s", "https://twinnation.org/health")
|
||||
}
|
||||
if config.Services[0].Interval != 60*time.Second {
|
||||
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
||||
}
|
||||
|
||||
if config.Web.Address != DefaultAddress {
|
||||
t.Errorf("Bind address should have been %s, because it is the default value", DefaultAddress)
|
||||
}
|
||||
|
||||
if config.Web.Port != 12345 {
|
||||
t.Errorf("Port should have been %d, because it is specified in config", 12345)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,12 @@ type Alert struct {
|
||||
|
||||
// Triggered is used to determine whether an alert has been triggered. When an alert is resolved, this value
|
||||
// should be set back to false. It is used to prevent the same alert from going out twice.
|
||||
//
|
||||
// This value should only be modified if the provider.AlertProvider's Send function does not return an error for an
|
||||
// alert that hasn't been triggered yet. This doubles as a lazy retry. The reason why this behavior isn't also
|
||||
// applied for alerts that are already triggered and has become "healthy" again is to prevent a case where, for
|
||||
// some reason, the alert provider always returns errors when trying to send the resolved notification
|
||||
// (SendOnResolved).
|
||||
Triggered bool
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -48,11 +47,20 @@ const (
|
||||
CertificateExpirationPlaceholder = "[CERTIFICATE_EXPIRATION]"
|
||||
|
||||
// LengthFunctionPrefix is the prefix for the length function
|
||||
//
|
||||
// Usage: len([BODY].articles) == 10, len([BODY].name) > 5
|
||||
LengthFunctionPrefix = "len("
|
||||
|
||||
// PatternFunctionPrefix is the prefix for the pattern function
|
||||
//
|
||||
// Usage: pat(192.168.*.*)
|
||||
PatternFunctionPrefix = "pat("
|
||||
|
||||
// AnyFunctionPrefix is the prefix for the any function
|
||||
//
|
||||
// Usage: any(1.1.1.1, 1.0.0.1)
|
||||
AnyFunctionPrefix = "any("
|
||||
|
||||
// FunctionSuffix is the suffix for all functions
|
||||
FunctionSuffix = ")"
|
||||
|
||||
@@ -64,51 +72,52 @@ const (
|
||||
type Condition string
|
||||
|
||||
// evaluate the Condition with the Result of the health check
|
||||
func (c *Condition) evaluate(result *Result) bool {
|
||||
condition := string(*c)
|
||||
func (c Condition) evaluate(result *Result) bool {
|
||||
condition := string(c)
|
||||
success := false
|
||||
var resolvedCondition string
|
||||
conditionToDisplay := condition
|
||||
if strings.Contains(condition, "==") {
|
||||
parts := sanitizeAndResolve(strings.Split(condition, "=="), result)
|
||||
success = isEqual(parts[0], parts[1])
|
||||
resolvedCondition = fmt.Sprintf("%v == %v", parts[0], parts[1])
|
||||
parameters, resolvedParameters := sanitizeAndResolve(strings.Split(condition, "=="), result)
|
||||
success = isEqual(resolvedParameters[0], resolvedParameters[1])
|
||||
if !success {
|
||||
conditionToDisplay = prettify(parameters, resolvedParameters, "==")
|
||||
}
|
||||
} else if strings.Contains(condition, "!=") {
|
||||
parts := sanitizeAndResolve(strings.Split(condition, "!="), result)
|
||||
success = !isEqual(parts[0], parts[1])
|
||||
resolvedCondition = fmt.Sprintf("%v != %v", parts[0], parts[1])
|
||||
parameters, resolvedParameters := sanitizeAndResolve(strings.Split(condition, "!="), result)
|
||||
success = !isEqual(resolvedParameters[0], resolvedParameters[1])
|
||||
if !success {
|
||||
conditionToDisplay = prettify(parameters, resolvedParameters, "!=")
|
||||
}
|
||||
} 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])
|
||||
parameters, resolvedParameters := sanitizeAndResolveNumerical(strings.Split(condition, "<="), result)
|
||||
success = resolvedParameters[0] <= resolvedParameters[1]
|
||||
if !success {
|
||||
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, "<=")
|
||||
}
|
||||
} 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])
|
||||
parameters, resolvedParameters := sanitizeAndResolveNumerical(strings.Split(condition, ">="), result)
|
||||
success = resolvedParameters[0] >= resolvedParameters[1]
|
||||
if !success {
|
||||
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, ">=")
|
||||
}
|
||||
} 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])
|
||||
parameters, resolvedParameters := sanitizeAndResolveNumerical(strings.Split(condition, ">"), result)
|
||||
success = resolvedParameters[0] > resolvedParameters[1]
|
||||
if !success {
|
||||
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, ">")
|
||||
}
|
||||
} 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])
|
||||
parameters, resolvedParameters := sanitizeAndResolveNumerical(strings.Split(condition, "<"), result)
|
||||
success = resolvedParameters[0] < resolvedParameters[1]
|
||||
if !success {
|
||||
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, "<")
|
||||
}
|
||||
} 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 what the resolved condition was too
|
||||
if !success {
|
||||
log.Printf("[Condition][evaluate] Condition '%s' did not succeed because '%s' is false", condition, resolvedCondition)
|
||||
// Check if the resolved condition was an invalid path
|
||||
isResolvedConditionInvalidPath := strings.ReplaceAll(resolvedCondition, fmt.Sprintf("%s ", InvalidConditionElementSuffix), "") == condition
|
||||
if isResolvedConditionInvalidPath {
|
||||
// Since, in the event of an invalid path, the resolvedCondition contains the condition itself,
|
||||
// we'll only display the resolvedCondition
|
||||
conditionToDisplay = resolvedCondition
|
||||
} else {
|
||||
conditionToDisplay = fmt.Sprintf("%s (%s)", condition, resolvedCondition)
|
||||
}
|
||||
//log.Printf("[Condition][evaluate] Condition '%s' did not succeed because '%s' is false", condition, condition)
|
||||
}
|
||||
result.ConditionResults = append(result.ConditionResults, &ConditionResult{Condition: conditionToDisplay, Success: success})
|
||||
return success
|
||||
@@ -116,33 +125,66 @@ func (c *Condition) evaluate(result *Result) bool {
|
||||
|
||||
// isEqual compares two strings.
|
||||
//
|
||||
// It also supports the pattern function. That is to say, if one of the strings starts with PatternFunctionPrefix
|
||||
// and ends with FunctionSuffix, it will be treated like a pattern.
|
||||
// Supports the pattern and the any functions.
|
||||
// i.e. if one of the parameters starts with PatternFunctionPrefix and ends with FunctionSuffix, it will be treated like
|
||||
// a pattern.
|
||||
func isEqual(first, second string) bool {
|
||||
var isFirstPattern, isSecondPattern bool
|
||||
if strings.HasPrefix(first, PatternFunctionPrefix) && strings.HasSuffix(first, FunctionSuffix) {
|
||||
isFirstPattern = true
|
||||
first = strings.TrimSuffix(strings.TrimPrefix(first, PatternFunctionPrefix), FunctionSuffix)
|
||||
}
|
||||
if strings.HasPrefix(second, PatternFunctionPrefix) && strings.HasSuffix(second, FunctionSuffix) {
|
||||
isSecondPattern = true
|
||||
second = strings.TrimSuffix(strings.TrimPrefix(second, PatternFunctionPrefix), FunctionSuffix)
|
||||
}
|
||||
if isFirstPattern && !isSecondPattern {
|
||||
return pattern.Match(first, second)
|
||||
} else if !isFirstPattern && isSecondPattern {
|
||||
return pattern.Match(second, first)
|
||||
} else {
|
||||
return first == second
|
||||
firstHasFunctionSuffix := strings.HasSuffix(first, FunctionSuffix)
|
||||
secondHasFunctionSuffix := strings.HasSuffix(second, FunctionSuffix)
|
||||
if firstHasFunctionSuffix || secondHasFunctionSuffix {
|
||||
var isFirstPattern, isSecondPattern bool
|
||||
if strings.HasPrefix(first, PatternFunctionPrefix) && firstHasFunctionSuffix {
|
||||
isFirstPattern = true
|
||||
first = strings.TrimSuffix(strings.TrimPrefix(first, PatternFunctionPrefix), FunctionSuffix)
|
||||
}
|
||||
if strings.HasPrefix(second, PatternFunctionPrefix) && secondHasFunctionSuffix {
|
||||
isSecondPattern = true
|
||||
second = strings.TrimSuffix(strings.TrimPrefix(second, PatternFunctionPrefix), FunctionSuffix)
|
||||
}
|
||||
if isFirstPattern && !isSecondPattern {
|
||||
return pattern.Match(first, second)
|
||||
} else if !isFirstPattern && isSecondPattern {
|
||||
return pattern.Match(second, first)
|
||||
}
|
||||
var isFirstAny, isSecondAny bool
|
||||
if strings.HasPrefix(first, AnyFunctionPrefix) && firstHasFunctionSuffix {
|
||||
isFirstAny = true
|
||||
first = strings.TrimSuffix(strings.TrimPrefix(first, AnyFunctionPrefix), FunctionSuffix)
|
||||
}
|
||||
if strings.HasPrefix(second, AnyFunctionPrefix) && secondHasFunctionSuffix {
|
||||
isSecondAny = true
|
||||
second = strings.TrimSuffix(strings.TrimPrefix(second, AnyFunctionPrefix), FunctionSuffix)
|
||||
}
|
||||
if isFirstAny && !isSecondAny {
|
||||
options := strings.Split(first, ",")
|
||||
for _, option := range options {
|
||||
if strings.TrimSpace(option) == second {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
} else if !isFirstAny && isSecondAny {
|
||||
options := strings.Split(second, ",")
|
||||
for _, option := range options {
|
||||
if strings.TrimSpace(option) == first {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
return first == second
|
||||
}
|
||||
|
||||
// sanitizeAndResolve sanitizes and resolves a list of element and returns the list of resolved elements
|
||||
func sanitizeAndResolve(list []string, result *Result) []string {
|
||||
var sanitizedList []string
|
||||
// sanitizeAndResolve sanitizes and resolves a list of elements and returns the list of parameters as well as a list
|
||||
// of resolved parameters
|
||||
func sanitizeAndResolve(elements []string, result *Result) ([]string, []string) {
|
||||
parameters := make([]string, len(elements))
|
||||
resolvedParameters := make([]string, len(elements))
|
||||
body := strings.TrimSpace(string(result.Body))
|
||||
for _, element := range list {
|
||||
for i, element := range elements {
|
||||
element = strings.TrimSpace(element)
|
||||
parameters[i] = element
|
||||
switch strings.ToUpper(element) {
|
||||
case StatusPlaceholder:
|
||||
element = strconv.Itoa(result.HTTPStatus)
|
||||
@@ -166,42 +208,68 @@ func sanitizeAndResolve(list []string, result *Result) []string {
|
||||
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.TrimPrefix(element, BodyPlaceholder+"."), result.Body)
|
||||
if err != nil {
|
||||
if err.Error() != "unexpected end of JSON input" {
|
||||
result.Errors = append(result.Errors, err.Error())
|
||||
}
|
||||
if wantLength {
|
||||
element = fmt.Sprintf("%s%s%s %s", LengthFunctionPrefix, element, FunctionSuffix, InvalidConditionElementSuffix)
|
||||
element = LengthFunctionPrefix + element + FunctionSuffix + " " + InvalidConditionElementSuffix
|
||||
} else {
|
||||
element = fmt.Sprintf("%s %s", element, InvalidConditionElementSuffix)
|
||||
element = element + " " + InvalidConditionElementSuffix
|
||||
}
|
||||
} else {
|
||||
if wantLength {
|
||||
element = fmt.Sprintf("%d", resolvedElementLength)
|
||||
element = strconv.Itoa(resolvedElementLength)
|
||||
} else {
|
||||
element = resolvedElement
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sanitizedList = append(sanitizedList, element)
|
||||
resolvedParameters[i] = element
|
||||
}
|
||||
return sanitizedList
|
||||
return parameters, resolvedParameters
|
||||
}
|
||||
|
||||
func sanitizeAndResolveNumerical(list []string, result *Result) []int64 {
|
||||
var sanitizedNumbers []int64
|
||||
sanitizedList := sanitizeAndResolve(list, result)
|
||||
for _, element := range sanitizedList {
|
||||
func sanitizeAndResolveNumerical(list []string, result *Result) (parameters []string, resolvedNumericalParameters []int64) {
|
||||
parameters, resolvedParameters := sanitizeAndResolve(list, result)
|
||||
for _, element := range resolvedParameters {
|
||||
if duration, err := time.ParseDuration(element); duration != 0 && err == nil {
|
||||
sanitizedNumbers = append(sanitizedNumbers, duration.Milliseconds())
|
||||
resolvedNumericalParameters = append(resolvedNumericalParameters, 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)
|
||||
resolvedNumericalParameters = append(resolvedNumericalParameters, 0)
|
||||
} else {
|
||||
sanitizedNumbers = append(sanitizedNumbers, number)
|
||||
resolvedNumericalParameters = append(resolvedNumericalParameters, number)
|
||||
}
|
||||
}
|
||||
return sanitizedNumbers
|
||||
return parameters, resolvedNumericalParameters
|
||||
}
|
||||
|
||||
func prettifyNumericalParameters(parameters []string, resolvedParameters []int64, operator string) string {
|
||||
return prettify(parameters, []string{strconv.Itoa(int(resolvedParameters[0])), strconv.Itoa(int(resolvedParameters[1]))}, operator)
|
||||
}
|
||||
|
||||
// XXX: make this configurable? i.e. show-resolved-conditions-on-failure
|
||||
func prettify(parameters []string, resolvedParameters []string, operator string) string {
|
||||
// Since, in the event of an invalid path, the resolvedParameters also contain the condition itself,
|
||||
// we'll return the resolvedParameters as-is.
|
||||
if strings.HasSuffix(resolvedParameters[0], InvalidConditionElementSuffix) || strings.HasSuffix(resolvedParameters[1], InvalidConditionElementSuffix) {
|
||||
return resolvedParameters[0] + " " + operator + " " + resolvedParameters[1]
|
||||
}
|
||||
// First element is a placeholder
|
||||
if parameters[0] != resolvedParameters[0] && parameters[1] == resolvedParameters[1] {
|
||||
return parameters[0] + " (" + resolvedParameters[0] + ") " + operator + " " + parameters[1]
|
||||
}
|
||||
// Second element is a placeholder
|
||||
if parameters[0] == resolvedParameters[0] && parameters[1] != resolvedParameters[1] {
|
||||
return parameters[0] + " " + operator + " " + parameters[1] + " (" + resolvedParameters[1] + ")"
|
||||
}
|
||||
// Both elements are placeholders...?
|
||||
if parameters[0] != resolvedParameters[0] && parameters[1] != resolvedParameters[1] {
|
||||
return parameters[0] + " (" + resolvedParameters[0] + ") " + operator + " " + parameters[1] + " (" + resolvedParameters[1] + ")"
|
||||
}
|
||||
// Neither elements are placeholders
|
||||
return parameters[0] + " " + operator + " " + parameters[1]
|
||||
}
|
||||
|
||||
75
core/condition_bench_test.go
Normal file
75
core/condition_bench_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package core
|
||||
|
||||
import "testing"
|
||||
|
||||
func BenchmarkCondition_evaluateWithBodyStringAny(b *testing.B) {
|
||||
condition := Condition("[BODY].name == any(john.doe, jane.doe)")
|
||||
for n := 0; n < b.N; n++ {
|
||||
result := &Result{Body: []byte("{\"name\": \"john.doe\"}")}
|
||||
condition.evaluate(result)
|
||||
}
|
||||
b.ReportAllocs()
|
||||
}
|
||||
|
||||
func BenchmarkCondition_evaluateWithBodyStringAnyFailure(b *testing.B) {
|
||||
condition := Condition("[BODY].name == any(john.doe, jane.doe)")
|
||||
for n := 0; n < b.N; n++ {
|
||||
result := &Result{Body: []byte("{\"name\": \"bob.doe\"}")}
|
||||
condition.evaluate(result)
|
||||
}
|
||||
b.ReportAllocs()
|
||||
}
|
||||
|
||||
func BenchmarkCondition_evaluateWithBodyString(b *testing.B) {
|
||||
condition := Condition("[BODY].name == john.doe")
|
||||
for n := 0; n < b.N; n++ {
|
||||
result := &Result{Body: []byte("{\"name\": \"john.doe\"}")}
|
||||
condition.evaluate(result)
|
||||
}
|
||||
b.ReportAllocs()
|
||||
}
|
||||
|
||||
func BenchmarkCondition_evaluateWithBodyStringFailure(b *testing.B) {
|
||||
condition := Condition("[BODY].name == john.doe")
|
||||
for n := 0; n < b.N; n++ {
|
||||
result := &Result{Body: []byte("{\"name\": \"bob.doe\"}")}
|
||||
condition.evaluate(result)
|
||||
}
|
||||
b.ReportAllocs()
|
||||
}
|
||||
|
||||
func BenchmarkCondition_evaluateWithBodyStringLen(b *testing.B) {
|
||||
condition := Condition("len([BODY].name) == 8")
|
||||
for n := 0; n < b.N; n++ {
|
||||
result := &Result{Body: []byte("{\"name\": \"john.doe\"}")}
|
||||
condition.evaluate(result)
|
||||
}
|
||||
b.ReportAllocs()
|
||||
}
|
||||
|
||||
func BenchmarkCondition_evaluateWithBodyStringLenFailure(b *testing.B) {
|
||||
condition := Condition("len([BODY].name) == 8")
|
||||
for n := 0; n < b.N; n++ {
|
||||
result := &Result{Body: []byte("{\"name\": \"bob.doe\"}")}
|
||||
condition.evaluate(result)
|
||||
}
|
||||
b.ReportAllocs()
|
||||
}
|
||||
|
||||
func BenchmarkCondition_evaluateWithStatus(b *testing.B) {
|
||||
condition := Condition("[STATUS] == 200")
|
||||
for n := 0; n < b.N; n++ {
|
||||
result := &Result{HTTPStatus: 200}
|
||||
condition.evaluate(result)
|
||||
}
|
||||
b.ReportAllocs()
|
||||
}
|
||||
|
||||
func BenchmarkCondition_evaluateWithStatusFailure(b *testing.B) {
|
||||
condition := Condition("[STATUS] == 200")
|
||||
for n := 0; n < b.N; n++ {
|
||||
result := &Result{HTTPStatus: 400}
|
||||
condition.evaluate(result)
|
||||
}
|
||||
b.ReportAllocs()
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -22,6 +23,9 @@ func TestCondition_evaluateWithStatus(t *testing.T) {
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
if result.ConditionResults[0].Condition != string(condition) {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, condition, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithStatusFailure(t *testing.T) {
|
||||
@@ -31,6 +35,10 @@ func TestCondition_evaluateWithStatusFailure(t *testing.T) {
|
||||
if result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a failure", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[STATUS] (500) == 200"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithStatusUsingLessThan(t *testing.T) {
|
||||
@@ -40,6 +48,10 @@ func TestCondition_evaluateWithStatusUsingLessThan(t *testing.T) {
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[STATUS] < 300"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithStatusFailureUsingLessThan(t *testing.T) {
|
||||
@@ -49,6 +61,10 @@ func TestCondition_evaluateWithStatusFailureUsingLessThan(t *testing.T) {
|
||||
if result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a failure", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[STATUS] (404) < 300"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithResponseTimeUsingLessThan(t *testing.T) {
|
||||
@@ -58,6 +74,10 @@ func TestCondition_evaluateWithResponseTimeUsingLessThan(t *testing.T) {
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[RESPONSE_TIME] < 500"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithResponseTimeUsingLessThanDuration(t *testing.T) {
|
||||
@@ -67,6 +87,10 @@ func TestCondition_evaluateWithResponseTimeUsingLessThanDuration(t *testing.T) {
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[RESPONSE_TIME] < 1s"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithResponseTimeUsingLessThanInvalid(t *testing.T) {
|
||||
@@ -76,6 +100,10 @@ func TestCondition_evaluateWithResponseTimeUsingLessThanInvalid(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
expectedConditionDisplayed := "[RESPONSE_TIME] (50) < potato (0)"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithResponseTimeUsingGreaterThan(t *testing.T) {
|
||||
@@ -87,6 +115,10 @@ func TestCondition_evaluateWithResponseTimeUsingGreaterThan(t *testing.T) {
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[RESPONSE_TIME] > 500"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithResponseTimeUsingGreaterThanDuration(t *testing.T) {
|
||||
@@ -96,6 +128,10 @@ func TestCondition_evaluateWithResponseTimeUsingGreaterThanDuration(t *testing.T
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[RESPONSE_TIME] > 1s"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithResponseTimeUsingGreaterThanOrEqualTo(t *testing.T) {
|
||||
@@ -105,6 +141,10 @@ func TestCondition_evaluateWithResponseTimeUsingGreaterThanOrEqualTo(t *testing.
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[RESPONSE_TIME] >= 500"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithResponseTimeUsingLessThanOrEqualTo(t *testing.T) {
|
||||
@@ -114,6 +154,10 @@ func TestCondition_evaluateWithResponseTimeUsingLessThanOrEqualTo(t *testing.T)
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[RESPONSE_TIME] <= 500"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithBody(t *testing.T) {
|
||||
@@ -123,6 +167,10 @@ func TestCondition_evaluateWithBody(t *testing.T) {
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[BODY] == test"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithBodyJSONPath(t *testing.T) {
|
||||
@@ -132,6 +180,10 @@ func TestCondition_evaluateWithBodyJSONPath(t *testing.T) {
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[BODY].status == UP"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithBodyJSONPathComplex(t *testing.T) {
|
||||
@@ -141,31 +193,35 @@ func TestCondition_evaluateWithBodyJSONPathComplex(t *testing.T) {
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[BODY].data.name == john"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithInvalidBodyJSONPathComplex(t *testing.T) {
|
||||
expectedResolvedCondition := "[BODY].data.name (INVALID) == john"
|
||||
condition := Condition("[BODY].data.name == john")
|
||||
result := &Result{Body: []byte("{\"data\": {\"id\": 1}}")}
|
||||
condition.evaluate(result)
|
||||
if result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a failure, because the path was invalid", condition)
|
||||
}
|
||||
if result.ConditionResults[0].Condition != expectedResolvedCondition {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', but resolved to '%s' instead", condition, expectedResolvedCondition, result.ConditionResults[0].Condition)
|
||||
expectedConditionDisplayed := "[BODY].data.name (INVALID) == john"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithInvalidBodyJSONPathComplexWithLengthFunction(t *testing.T) {
|
||||
expectedResolvedCondition := "len([BODY].data.name) (INVALID) == john"
|
||||
condition := Condition("len([BODY].data.name) == john")
|
||||
result := &Result{Body: []byte("{\"data\": {\"id\": 1}}")}
|
||||
condition.evaluate(result)
|
||||
if result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a failure, because the path was invalid", condition)
|
||||
}
|
||||
if result.ConditionResults[0].Condition != expectedResolvedCondition {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', but resolved to '%s' instead", condition, expectedResolvedCondition, result.ConditionResults[0].Condition)
|
||||
expectedConditionDisplayed := "len([BODY].data.name) (INVALID) == john"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +232,10 @@ func TestCondition_evaluateWithBodyJSONPathDoublePlaceholders(t *testing.T) {
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[BODY].user.firstName != [BODY].user.lastName"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithBodyJSONPathDoublePlaceholdersFailure(t *testing.T) {
|
||||
@@ -185,6 +245,10 @@ func TestCondition_evaluateWithBodyJSONPathDoublePlaceholdersFailure(t *testing.
|
||||
if result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a failure", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[BODY].user.firstName (john) == [BODY].user.lastName (doe)"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithBodyJSONPathLongInt(t *testing.T) {
|
||||
@@ -194,6 +258,10 @@ func TestCondition_evaluateWithBodyJSONPathLongInt(t *testing.T) {
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[BODY].data.id == 1"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithBodyJSONPathComplexInt(t *testing.T) {
|
||||
@@ -203,6 +271,10 @@ func TestCondition_evaluateWithBodyJSONPathComplexInt(t *testing.T) {
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[BODY].data[1].id == 2"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithBodyJSONPathComplexIntUsingGreaterThan(t *testing.T) {
|
||||
@@ -212,6 +284,10 @@ func TestCondition_evaluateWithBodyJSONPathComplexIntUsingGreaterThan(t *testing
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[BODY].data.id > 0"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithBodyJSONPathComplexIntFailureUsingGreaterThan(t *testing.T) {
|
||||
@@ -221,6 +297,10 @@ func TestCondition_evaluateWithBodyJSONPathComplexIntFailureUsingGreaterThan(t *
|
||||
if result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a failure", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[BODY].data.id (1) > 5"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithBodyJSONPathComplexIntUsingLessThan(t *testing.T) {
|
||||
@@ -230,6 +310,10 @@ func TestCondition_evaluateWithBodyJSONPathComplexIntUsingLessThan(t *testing.T)
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[BODY].data.id < 5"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithBodyJSONPathComplexIntFailureUsingLessThan(t *testing.T) {
|
||||
@@ -239,6 +323,10 @@ func TestCondition_evaluateWithBodyJSONPathComplexIntFailureUsingLessThan(t *tes
|
||||
if result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a failure", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[BODY].data.id (10) < 5"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithBodySliceLength(t *testing.T) {
|
||||
@@ -248,6 +336,10 @@ func TestCondition_evaluateWithBodySliceLength(t *testing.T) {
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "len([BODY].data) == 3"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithBodyStringLength(t *testing.T) {
|
||||
@@ -257,6 +349,36 @@ func TestCondition_evaluateWithBodyStringLength(t *testing.T) {
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "len([BODY].name) == 8"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithBodyPattern(t *testing.T) {
|
||||
condition := Condition("[BODY] == pat(*john*)")
|
||||
result := &Result{Body: []byte("{\"name\": \"john.doe\"}")}
|
||||
condition.evaluate(result)
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[BODY] == pat(*john*)"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithReverseBodyPattern(t *testing.T) {
|
||||
condition := Condition("pat(*john*) == [BODY]")
|
||||
result := &Result{Body: []byte("{\"name\": \"john.doe\"}")}
|
||||
condition.evaluate(result)
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "pat(*john*) == [BODY]"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithBodyStringPattern(t *testing.T) {
|
||||
@@ -266,6 +388,24 @@ func TestCondition_evaluateWithBodyStringPattern(t *testing.T) {
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[BODY].name == pat(*ohn*)"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithBodyHTMLPattern(t *testing.T) {
|
||||
var html = `<!DOCTYPE html><html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /></head><body><div id="user">john.doe</div></body></html>`
|
||||
condition := Condition("[BODY] == pat(*<div id=\"user\">john.doe</div>*)")
|
||||
result := &Result{Body: []byte(html)}
|
||||
condition.evaluate(result)
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[BODY] == pat(*<div id=\"user\">john.doe</div>*)"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithBodyStringPatternFailure(t *testing.T) {
|
||||
@@ -275,14 +415,9 @@ func TestCondition_evaluateWithBodyStringPatternFailure(t *testing.T) {
|
||||
if result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a failure", condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithBodyPatternFailure(t *testing.T) {
|
||||
condition := Condition("[BODY] == pat(*john*)")
|
||||
result := &Result{Body: []byte("{\"name\": \"john.doe\"}")}
|
||||
condition.evaluate(result)
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
expectedConditionDisplayed := "[BODY].name (john.doe) == pat(bob*)"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,6 +428,10 @@ func TestCondition_evaluateWithIPPattern(t *testing.T) {
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[IP] == pat(10.*)"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithIPPatternFailure(t *testing.T) {
|
||||
@@ -302,6 +441,10 @@ func TestCondition_evaluateWithIPPatternFailure(t *testing.T) {
|
||||
if result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a failure", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[IP] (255.255.255.255) == pat(10.*)"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithStatusPattern(t *testing.T) {
|
||||
@@ -311,6 +454,10 @@ func TestCondition_evaluateWithStatusPattern(t *testing.T) {
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[STATUS] == pat(4*)"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithStatusPatternFailure(t *testing.T) {
|
||||
@@ -320,6 +467,105 @@ func TestCondition_evaluateWithStatusPatternFailure(t *testing.T) {
|
||||
if result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a failure", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[STATUS] (404) != pat(4*)"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithBodyStringAny(t *testing.T) {
|
||||
condition := Condition("[BODY].name == any(john.doe, jane.doe)")
|
||||
expectedConditionDisplayed := "[BODY].name == any(john.doe, jane.doe)"
|
||||
results := []*Result{
|
||||
{Body: []byte("{\"name\": \"john.doe\"}")},
|
||||
{Body: []byte("{\"name\": \"jane.doe\"}")},
|
||||
}
|
||||
for _, result := range results {
|
||||
success := condition.evaluate(result)
|
||||
if !success || !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithBodyStringAnyFailure(t *testing.T) {
|
||||
condition := Condition("[BODY].name == any(john.doe, jane.doe)")
|
||||
result := &Result{Body: []byte("{\"name\": \"bob.doe\"}")}
|
||||
condition.evaluate(result)
|
||||
if result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a failure", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[BODY].name (bob.doe) == any(john.doe, jane.doe)"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithStatusAny(t *testing.T) {
|
||||
condition := Condition("[STATUS] == any(200, 429)")
|
||||
statuses := []int{200, 429}
|
||||
for _, status := range statuses {
|
||||
result := &Result{HTTPStatus: status}
|
||||
condition.evaluate(result)
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[STATUS] == any(200, 429)"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithReverseStatusAny(t *testing.T) {
|
||||
condition := Condition("any(200, 429) == [STATUS]")
|
||||
statuses := []int{200, 429}
|
||||
for _, status := range statuses {
|
||||
result := &Result{HTTPStatus: status}
|
||||
condition.evaluate(result)
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "any(200, 429) == [STATUS]"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithStatusAnyFailure(t *testing.T) {
|
||||
condition := Condition("[STATUS] == any(200, 429)")
|
||||
statuses := []int{201, 400, 404, 500}
|
||||
for _, status := range statuses {
|
||||
result := &Result{HTTPStatus: status}
|
||||
condition.evaluate(result)
|
||||
if result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a failure", condition)
|
||||
}
|
||||
expectedConditionDisplayed := fmt.Sprintf("[STATUS] (%d) == any(200, 429)", status)
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithReverseStatusAnyFailure(t *testing.T) {
|
||||
condition := Condition("any(200, 429) == [STATUS]")
|
||||
statuses := []int{201, 400, 404, 500}
|
||||
for _, status := range statuses {
|
||||
result := &Result{HTTPStatus: status}
|
||||
condition.evaluate(result)
|
||||
if result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a failure", condition)
|
||||
}
|
||||
expectedConditionDisplayed := fmt.Sprintf("any(200, 429) == [STATUS] (%d)", status)
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithConnected(t *testing.T) {
|
||||
@@ -329,6 +575,10 @@ func TestCondition_evaluateWithConnected(t *testing.T) {
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[CONNECTED] == true"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithConnectedFailure(t *testing.T) {
|
||||
@@ -338,6 +588,10 @@ func TestCondition_evaluateWithConnectedFailure(t *testing.T) {
|
||||
if result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a failure", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[CONNECTED] (false) == true"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithUnsetCertificateExpiration(t *testing.T) {
|
||||
@@ -347,6 +601,10 @@ func TestCondition_evaluateWithUnsetCertificateExpiration(t *testing.T) {
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[CERTIFICATE_EXPIRATION] == 0"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithCertificateExpirationGreaterThanNumerical(t *testing.T) {
|
||||
@@ -357,6 +615,10 @@ func TestCondition_evaluateWithCertificateExpirationGreaterThanNumerical(t *test
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[CERTIFICATE_EXPIRATION] > 2419200000"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithCertificateExpirationGreaterThanNumericalFailure(t *testing.T) {
|
||||
@@ -367,6 +629,10 @@ func TestCondition_evaluateWithCertificateExpirationGreaterThanNumericalFailure(
|
||||
if result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a failure", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[CERTIFICATE_EXPIRATION] (1209600000) > 2419200000"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithCertificateExpirationGreaterThanDuration(t *testing.T) {
|
||||
@@ -376,6 +642,10 @@ func TestCondition_evaluateWithCertificateExpirationGreaterThanDuration(t *testi
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[CERTIFICATE_EXPIRATION] > 12h"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCondition_evaluateWithCertificateExpirationGreaterThanDurationFailure(t *testing.T) {
|
||||
@@ -385,4 +655,8 @@ func TestCondition_evaluateWithCertificateExpirationGreaterThanDurationFailure(t
|
||||
if result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a failure", condition)
|
||||
}
|
||||
expectedConditionDisplayed := "[CERTIFICATE_EXPIRATION] (86400000) > 48h (172800000)"
|
||||
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
|
||||
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
alerting:
|
||||
mattermost:
|
||||
webhook-url: "http://mattermost:8065/hooks/tokengoeshere"
|
||||
insecure: true
|
||||
|
||||
services:
|
||||
- name: example
|
||||
url: http://example.org
|
||||
interval: 30s
|
||||
interval: 1m
|
||||
alerts:
|
||||
- type: mattermost
|
||||
enabled: true
|
||||
description: "healthcheck failed 3 times in a row"
|
||||
send-on-resolved: true
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
gatus:
|
||||
image: twinproduction/gatus:latest
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- ./config.yaml:/config/config.yaml
|
||||
|
||||
mattermost-preview:
|
||||
image: mattermost/mattermost-preview:latest
|
||||
ports:
|
||||
- 8065:8065
|
||||
services:
|
||||
gatus:
|
||||
container_name: gatus
|
||||
image: twinproduction/gatus:latest
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- ./config.yaml:/config/config.yaml
|
||||
networks:
|
||||
- default
|
||||
|
||||
mattermost:
|
||||
container_name: mattermost
|
||||
image: mattermost/mattermost-preview:5.26.0
|
||||
ports:
|
||||
- 8065:8065
|
||||
networks:
|
||||
- default
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
|
||||
2
go.mod
2
go.mod
@@ -4,7 +4,7 @@ go 1.15
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.74.0 // indirect
|
||||
github.com/TwinProduction/gocache v0.3.0
|
||||
github.com/TwinProduction/gocache v1.1.0
|
||||
github.com/go-ping/ping v0.0.0-20201115131931-3300c582a663
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/gorilla/mux v1.8.0
|
||||
|
||||
4
go.sum
4
go.sum
@@ -50,8 +50,8 @@ github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/TwinProduction/gocache v0.3.0 h1:nC02PSOyGLiXGrOJ6eskGc5chBq5GW6m3pA2g341VEM=
|
||||
github.com/TwinProduction/gocache v0.3.0/go.mod h1:+qH57V/K4oAcX9C7CvgJTwUX4lzfIUXQC/6XaRSOS1Y=
|
||||
github.com/TwinProduction/gocache v1.1.0 h1:mibBUyccd8kGHlm5dXhTMDOvWBK4mjNqGyOOkG8mib8=
|
||||
github.com/TwinProduction/gocache v1.1.0/go.mod h1:+qH57V/K4oAcX9C7CvgJTwUX4lzfIUXQC/6XaRSOS1Y=
|
||||
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
|
||||
@@ -10,10 +10,11 @@ func Match(pattern, s string) bool {
|
||||
if pattern == "*" {
|
||||
return true
|
||||
}
|
||||
// Backslashes break filepath.Match, so we'll remove all of them.
|
||||
// This has a pretty significant impact on performance when there
|
||||
// are backslashes, but at least it doesn't break filepath.Match.
|
||||
s = strings.ReplaceAll(s, "\\", "")
|
||||
// Separators found in the string break filepath.Match, so we'll remove all of them.
|
||||
// This has a pretty significant impact on performance when there are separators in
|
||||
// the strings, but at least it doesn't break filepath.Match.
|
||||
s = strings.ReplaceAll(s, string(filepath.Separator), "")
|
||||
pattern = strings.ReplaceAll(pattern, string(filepath.Separator), "")
|
||||
matched, _ := filepath.Match(pattern, s)
|
||||
return matched
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
@@ -22,19 +23,12 @@ func NewInMemoryStore() *InMemoryStore {
|
||||
}
|
||||
}
|
||||
|
||||
// GetAll returns all the observed results for all services from the in memory store
|
||||
func (ims *InMemoryStore) GetAll() map[string]*core.ServiceStatus {
|
||||
results := make(map[string]*core.ServiceStatus, len(ims.serviceStatuses))
|
||||
// GetAllAsJSON returns the JSON encoding of all monitored core.ServiceStatus
|
||||
func (ims *InMemoryStore) GetAllAsJSON() ([]byte, error) {
|
||||
ims.serviceResultsMutex.RLock()
|
||||
for key, serviceStatus := range ims.serviceStatuses {
|
||||
results[key] = &core.ServiceStatus{
|
||||
Name: serviceStatus.Name,
|
||||
Group: serviceStatus.Group,
|
||||
Results: copyResults(serviceStatus.Results),
|
||||
}
|
||||
}
|
||||
serviceStatuses, err := json.Marshal(ims.serviceStatuses)
|
||||
ims.serviceResultsMutex.RUnlock()
|
||||
return results
|
||||
return serviceStatuses, err
|
||||
}
|
||||
|
||||
// GetServiceStatus returns the service status for a given service name in the given group
|
||||
@@ -59,46 +53,6 @@ func (ims *InMemoryStore) Insert(service *core.Service, result *core.Result) {
|
||||
ims.serviceResultsMutex.Unlock()
|
||||
}
|
||||
|
||||
func copyResults(results []*core.Result) []*core.Result {
|
||||
var copiedResults []*core.Result
|
||||
for _, result := range results {
|
||||
copiedResults = append(copiedResults, &core.Result{
|
||||
HTTPStatus: result.HTTPStatus,
|
||||
DNSRCode: result.DNSRCode,
|
||||
Body: result.Body,
|
||||
Hostname: result.Hostname,
|
||||
IP: result.IP,
|
||||
Connected: result.Connected,
|
||||
Duration: result.Duration,
|
||||
Errors: copyErrors(result.Errors),
|
||||
ConditionResults: copyConditionResults(result.ConditionResults),
|
||||
Success: result.Connected,
|
||||
Timestamp: result.Timestamp,
|
||||
CertificateExpiration: result.CertificateExpiration,
|
||||
})
|
||||
}
|
||||
return copiedResults
|
||||
}
|
||||
|
||||
func copyConditionResults(conditionResults []*core.ConditionResult) []*core.ConditionResult {
|
||||
var copiedConditionResults []*core.ConditionResult
|
||||
for _, conditionResult := range conditionResults {
|
||||
copiedConditionResults = append(copiedConditionResults, &core.ConditionResult{
|
||||
Condition: conditionResult.Condition,
|
||||
Success: conditionResult.Success,
|
||||
})
|
||||
}
|
||||
return copiedConditionResults
|
||||
}
|
||||
|
||||
func copyErrors(errors []string) []string {
|
||||
var copiedErrors []string
|
||||
for _, err := range errors {
|
||||
copiedErrors = append(copiedErrors, err)
|
||||
}
|
||||
return copiedErrors
|
||||
}
|
||||
|
||||
// Clear will empty all the results from the in memory store
|
||||
func (ims *InMemoryStore) Clear() {
|
||||
ims.serviceResultsMutex.Lock()
|
||||
|
||||
@@ -8,131 +8,98 @@ import (
|
||||
"github.com/TwinProduction/gatus/core"
|
||||
)
|
||||
|
||||
var testService = core.Service{
|
||||
Name: "Name",
|
||||
Group: "Group",
|
||||
URL: "URL",
|
||||
DNS: &core.DNS{QueryType: "QueryType", QueryName: "QueryName"},
|
||||
Method: "Method",
|
||||
Body: "Body",
|
||||
GraphQL: false,
|
||||
Headers: nil,
|
||||
Interval: time.Second * 2,
|
||||
Conditions: nil,
|
||||
Alerts: nil,
|
||||
Insecure: false,
|
||||
NumberOfFailuresInARow: 0,
|
||||
NumberOfSuccessesInARow: 0,
|
||||
}
|
||||
var (
|
||||
firstCondition = core.Condition("[STATUS] == 200")
|
||||
secondCondition = core.Condition("[RESPONSE_TIME] < 500")
|
||||
thirdCondition = core.Condition("[CERTIFICATE_EXPIRATION] < 72h")
|
||||
|
||||
var memoryStore = NewInMemoryStore()
|
||||
timestamp = time.Now()
|
||||
|
||||
func TestStorage_GetAllFromEmptyMemoryStoreReturnsNothing(t *testing.T) {
|
||||
memoryStore.Clear()
|
||||
results := memoryStore.GetAll()
|
||||
if len(results) != 0 {
|
||||
t.Errorf("MemoryStore should've returned 0 results, but actually returned %d", len(results))
|
||||
testService = core.Service{
|
||||
Name: "name",
|
||||
Group: "group",
|
||||
URL: "https://example.org/what/ever",
|
||||
Method: "GET",
|
||||
Body: "body",
|
||||
Interval: 30 * time.Second,
|
||||
Conditions: []*core.Condition{&firstCondition, &secondCondition, &thirdCondition},
|
||||
Alerts: nil,
|
||||
Insecure: false,
|
||||
NumberOfFailuresInARow: 0,
|
||||
NumberOfSuccessesInARow: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_InsertIntoEmptyMemoryStoreThenGetAllReturnsOneResult(t *testing.T) {
|
||||
memoryStore.Clear()
|
||||
result := core.Result{
|
||||
testSuccessfulResult = core.Result{
|
||||
Hostname: "example.org",
|
||||
IP: "127.0.0.1",
|
||||
HTTPStatus: 200,
|
||||
DNSRCode: "DNSRCode",
|
||||
Body: nil,
|
||||
Hostname: "Hostname",
|
||||
IP: "IP",
|
||||
Connected: false,
|
||||
Duration: time.Second * 2,
|
||||
Body: []byte("body"),
|
||||
Errors: nil,
|
||||
ConditionResults: nil,
|
||||
Success: false,
|
||||
Timestamp: time.Now(),
|
||||
CertificateExpiration: time.Second * 2,
|
||||
}
|
||||
|
||||
memoryStore.Insert(&testService, &result)
|
||||
|
||||
results := memoryStore.GetAll()
|
||||
if len(results) != 1 {
|
||||
t.Errorf("MemoryStore should've returned 0 results, but actually returned %d", len(results))
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s_%s", testService.Group, testService.Name)
|
||||
storedResult, exists := results[key]
|
||||
if !exists {
|
||||
t.Fatalf("In Memory Store should've contained key '%s', but didn't", key)
|
||||
}
|
||||
|
||||
if storedResult.Name != testService.Name {
|
||||
t.Errorf("Stored Results Name should've been %s, but was %s", testService.Name, storedResult.Name)
|
||||
}
|
||||
if storedResult.Group != testService.Group {
|
||||
t.Errorf("Stored Results Group should've been %s, but was %s", testService.Group, storedResult.Group)
|
||||
}
|
||||
if len(storedResult.Results) != 1 {
|
||||
t.Errorf("Stored Results for service %s should've had 1 result, but actually had %d", storedResult.Name, len(storedResult.Results))
|
||||
}
|
||||
if storedResult.Results[0] == &result {
|
||||
t.Errorf("Returned result is the same reference as result passed to insert. Returned result should be copies only")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_InsertTwoResultsForSingleServiceIntoEmptyMemoryStore_ThenGetAllReturnsTwoResults(t *testing.T) {
|
||||
memoryStore.Clear()
|
||||
result1 := core.Result{
|
||||
HTTPStatus: 404,
|
||||
DNSRCode: "DNSRCode",
|
||||
Body: nil,
|
||||
Hostname: "Hostname",
|
||||
IP: "IP",
|
||||
Connected: false,
|
||||
Duration: time.Second * 2,
|
||||
Errors: nil,
|
||||
ConditionResults: nil,
|
||||
Success: false,
|
||||
Timestamp: time.Now(),
|
||||
CertificateExpiration: time.Second * 2,
|
||||
}
|
||||
result2 := core.Result{
|
||||
HTTPStatus: 200,
|
||||
DNSRCode: "DNSRCode",
|
||||
Body: nil,
|
||||
Hostname: "Hostname",
|
||||
IP: "IP",
|
||||
Connected: true,
|
||||
Duration: time.Second * 2,
|
||||
Errors: nil,
|
||||
ConditionResults: nil,
|
||||
Success: true,
|
||||
Timestamp: time.Now(),
|
||||
CertificateExpiration: time.Second * 2,
|
||||
Timestamp: timestamp,
|
||||
Duration: 150 * time.Millisecond,
|
||||
CertificateExpiration: 10 * time.Hour,
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
{
|
||||
Condition: "[STATUS] == 200",
|
||||
Success: true,
|
||||
},
|
||||
{
|
||||
Condition: "[RESPONSE_TIME] < 500",
|
||||
Success: true,
|
||||
},
|
||||
{
|
||||
Condition: "[CERTIFICATE_EXPIRATION] < 72h",
|
||||
Success: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resultsToInsert := []core.Result{result1, result2}
|
||||
|
||||
memoryStore.Insert(&testService, &result1)
|
||||
memoryStore.Insert(&testService, &result2)
|
||||
|
||||
results := memoryStore.GetAll()
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("MemoryStore should've returned 1 results, but actually returned %d", len(results))
|
||||
testUnsuccessfulResult = core.Result{
|
||||
Hostname: "example.org",
|
||||
IP: "127.0.0.1",
|
||||
HTTPStatus: 200,
|
||||
Body: []byte("body"),
|
||||
Errors: []string{"error-1", "error-2"},
|
||||
Connected: true,
|
||||
Success: false,
|
||||
Timestamp: timestamp,
|
||||
Duration: 750 * time.Millisecond,
|
||||
CertificateExpiration: 10 * time.Hour,
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
{
|
||||
Condition: "[STATUS] == 200",
|
||||
Success: true,
|
||||
},
|
||||
{
|
||||
Condition: "[RESPONSE_TIME] < 500",
|
||||
Success: false,
|
||||
},
|
||||
{
|
||||
Condition: "[CERTIFICATE_EXPIRATION] < 72h",
|
||||
Success: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func TestInMemoryStore_Insert(t *testing.T) {
|
||||
store := NewInMemoryStore()
|
||||
store.Insert(&testService, &testSuccessfulResult)
|
||||
store.Insert(&testService, &testUnsuccessfulResult)
|
||||
|
||||
if len(store.serviceStatuses) != 1 {
|
||||
t.Fatalf("expected 1 ServiceStatus, got %d", len(store.serviceStatuses))
|
||||
}
|
||||
key := fmt.Sprintf("%s_%s", testService.Group, testService.Name)
|
||||
serviceResults, exists := results[key]
|
||||
serviceStatus, exists := store.serviceStatuses[key]
|
||||
if !exists {
|
||||
t.Fatalf("In Memory Store should've contained key '%s', but didn't", key)
|
||||
t.Fatalf("Store should've had key '%s', but didn't", key)
|
||||
}
|
||||
|
||||
if len(serviceResults.Results) != 2 {
|
||||
t.Fatalf("Service '%s' should've had 2 results, but actually returned %d", serviceResults.Name, len(serviceResults.Results))
|
||||
if len(serviceStatus.Results) != 2 {
|
||||
t.Fatalf("Service '%s' should've had 2 results, but actually returned %d", serviceStatus.Name, len(serviceStatus.Results))
|
||||
}
|
||||
|
||||
for i, r := range serviceResults.Results {
|
||||
expectedResult := resultsToInsert[i]
|
||||
|
||||
for i, r := range serviceStatus.Results {
|
||||
expectedResult := store.GetServiceStatus(testService.Group, testService.Name).Results[i]
|
||||
if r.HTTPStatus != expectedResult.HTTPStatus {
|
||||
t.Errorf("Result at index %d should've had a HTTPStatus of %d, but was actually %d", i, expectedResult.HTTPStatus, r.HTTPStatus)
|
||||
}
|
||||
@@ -172,265 +139,63 @@ func TestStorage_InsertTwoResultsForSingleServiceIntoEmptyMemoryStore_ThenGetAll
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_InsertTwoResultsTwoServicesIntoEmptyMemoryStore_ThenGetAllReturnsTwoServicesWithOneResultEach(t *testing.T) {
|
||||
memoryStore.Clear()
|
||||
result1 := core.Result{
|
||||
HTTPStatus: 404,
|
||||
DNSRCode: "DNSRCode",
|
||||
Body: nil,
|
||||
Hostname: "Hostname",
|
||||
IP: "IP",
|
||||
Connected: false,
|
||||
Duration: time.Second * 2,
|
||||
Errors: nil,
|
||||
ConditionResults: nil,
|
||||
Success: false,
|
||||
Timestamp: time.Now(),
|
||||
CertificateExpiration: time.Second * 2,
|
||||
}
|
||||
result2 := core.Result{
|
||||
HTTPStatus: 200,
|
||||
DNSRCode: "DNSRCode",
|
||||
Body: nil,
|
||||
Hostname: "Hostname",
|
||||
IP: "IP",
|
||||
Connected: true,
|
||||
Duration: time.Second * 2,
|
||||
Errors: nil,
|
||||
ConditionResults: nil,
|
||||
Success: true,
|
||||
Timestamp: time.Now(),
|
||||
CertificateExpiration: time.Second * 2,
|
||||
}
|
||||
|
||||
testService2 := core.Service{
|
||||
Name: "Name2",
|
||||
Group: "Group",
|
||||
URL: "URL",
|
||||
DNS: &core.DNS{QueryType: "QueryType", QueryName: "QueryName"},
|
||||
Method: "Method",
|
||||
Body: "Body",
|
||||
GraphQL: false,
|
||||
Headers: nil,
|
||||
Interval: time.Second * 2,
|
||||
Conditions: nil,
|
||||
Alerts: nil,
|
||||
Insecure: false,
|
||||
NumberOfFailuresInARow: 0,
|
||||
NumberOfSuccessesInARow: 0,
|
||||
}
|
||||
|
||||
memoryStore.Insert(&testService, &result1)
|
||||
memoryStore.Insert(&testService2, &result2)
|
||||
|
||||
results := memoryStore.GetAll()
|
||||
if len(results) != 2 {
|
||||
t.Fatalf("MemoryStore should've returned 2 results, but actually returned %d", len(results))
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s_%s", testService.Group, testService.Name)
|
||||
serviceResults1, exists := results[key]
|
||||
if !exists {
|
||||
t.Fatalf("In Memory Store should've contained key '%s', but didn't", key)
|
||||
}
|
||||
|
||||
if len(serviceResults1.Results) != 1 {
|
||||
t.Fatalf("Service '%s' should've had 1 results, but actually returned %d", serviceResults1.Name, len(serviceResults1.Results))
|
||||
}
|
||||
|
||||
key = fmt.Sprintf("%s_%s", testService2.Group, testService2.Name)
|
||||
serviceResults2, exists := results[key]
|
||||
if !exists {
|
||||
t.Fatalf("In Memory Store should've contained key '%s', but didn't", key)
|
||||
}
|
||||
|
||||
if len(serviceResults2.Results) != 1 {
|
||||
t.Fatalf("Service '%s' should've had 1 results, but actually returned %d", serviceResults1.Name, len(serviceResults1.Results))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_InsertResultForServiceWithErrorsIntoEmptyMemoryStore_ThenGetAllReturnsOneResultWithErrors(t *testing.T) {
|
||||
memoryStore.Clear()
|
||||
errors := []string{
|
||||
"error1",
|
||||
"error2",
|
||||
}
|
||||
result1 := core.Result{
|
||||
HTTPStatus: 404,
|
||||
DNSRCode: "DNSRCode",
|
||||
Body: nil,
|
||||
Hostname: "Hostname",
|
||||
IP: "IP",
|
||||
Connected: false,
|
||||
Duration: time.Second * 2,
|
||||
Errors: errors,
|
||||
ConditionResults: nil,
|
||||
Success: false,
|
||||
Timestamp: time.Now(),
|
||||
CertificateExpiration: time.Second * 2,
|
||||
}
|
||||
|
||||
memoryStore.Insert(&testService, &result1)
|
||||
|
||||
results := memoryStore.GetAll()
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("MemoryStore should've returned 1 results, but actually returned %d", len(results))
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s_%s", testService.Group, testService.Name)
|
||||
serviceResults, exists := results[key]
|
||||
if !exists {
|
||||
t.Fatalf("In Memory Store should've contained key '%s', but didn't", key)
|
||||
}
|
||||
|
||||
if len(serviceResults.Results) != 1 {
|
||||
t.Fatalf("Service '%s' should've had 2 results, but actually returned %d", serviceResults.Name, len(serviceResults.Results))
|
||||
}
|
||||
|
||||
actualResult := serviceResults.Results[0]
|
||||
|
||||
if len(actualResult.Errors) != len(errors) {
|
||||
t.Errorf("Service result should've had 2 errors, but actually had %d errors", len(actualResult.Errors))
|
||||
}
|
||||
|
||||
for i, err := range actualResult.Errors {
|
||||
if err != errors[i] {
|
||||
t.Errorf("Error at index %d should've been %s, but was actually %s", i, errors[i], err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_InsertResultForServiceWithConditionResultsIntoEmptyMemoryStore_ThenGetAllReturnsOneResultWithConditionResults(t *testing.T) {
|
||||
memoryStore.Clear()
|
||||
crs := []*core.ConditionResult{
|
||||
{
|
||||
Condition: "condition1",
|
||||
Success: true,
|
||||
},
|
||||
{
|
||||
Condition: "condition2",
|
||||
Success: false,
|
||||
},
|
||||
}
|
||||
result := core.Result{
|
||||
HTTPStatus: 404,
|
||||
DNSRCode: "DNSRCode",
|
||||
Body: nil,
|
||||
Hostname: "Hostname",
|
||||
IP: "IP",
|
||||
Connected: false,
|
||||
Duration: time.Second * 2,
|
||||
Errors: nil,
|
||||
ConditionResults: crs,
|
||||
Success: false,
|
||||
Timestamp: time.Now(),
|
||||
CertificateExpiration: time.Second * 2,
|
||||
}
|
||||
|
||||
memoryStore.Insert(&testService, &result)
|
||||
|
||||
results := memoryStore.GetAll()
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("MemoryStore should've returned 1 results, but actually returned %d", len(results))
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s_%s", testService.Group, testService.Name)
|
||||
serviceResults, exists := results[key]
|
||||
if !exists {
|
||||
t.Fatalf("In Memory Store should've contained key '%s', but didn't", key)
|
||||
}
|
||||
|
||||
if len(serviceResults.Results) != 1 {
|
||||
t.Fatalf("Service '%s' should've had 2 results, but actually returned %d", serviceResults.Name, len(serviceResults.Results))
|
||||
}
|
||||
|
||||
actualResult := serviceResults.Results[0]
|
||||
|
||||
if len(actualResult.ConditionResults) != len(crs) {
|
||||
t.Errorf("Service result should've had 2 ConditionResults, but actually had %d ConditionResults", len(actualResult.Errors))
|
||||
}
|
||||
|
||||
for i, cr := range actualResult.ConditionResults {
|
||||
if cr.Condition != crs[i].Condition {
|
||||
t.Errorf("ConditionResult at index %d should've had condition %s, but was actually %s", i, crs[i].Condition, cr.Condition)
|
||||
}
|
||||
if cr.Success != crs[i].Success {
|
||||
t.Errorf("ConditionResult at index %d should've had success value of %t, but was actually %t", i, crs[i].Success, cr.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_MultipleMemoryStoreInstancesReferToDifferentInternalMaps(t *testing.T) {
|
||||
memoryStore.Clear()
|
||||
currentMap := memoryStore.GetAll()
|
||||
|
||||
otherMemoryStore := NewInMemoryStore()
|
||||
otherMemoryStoresMap := otherMemoryStore.GetAll()
|
||||
|
||||
if len(currentMap) != len(otherMemoryStoresMap) {
|
||||
t.Errorf("Multiple memory stores should refer to the different internal maps, but 'memoryStore' returned %d results, and 'otherMemoryStore' returned %d results", len(currentMap), len(otherMemoryStoresMap))
|
||||
}
|
||||
|
||||
memoryStore.Insert(&testService, &core.Result{})
|
||||
currentMap = memoryStore.GetAll()
|
||||
otherMemoryStoresMap = otherMemoryStore.GetAll()
|
||||
|
||||
if len(currentMap) == len(otherMemoryStoresMap) {
|
||||
t.Errorf("Multiple memory stores should refer to different internal maps, but 'memoryStore' returned %d results after inserting, and 'otherMemoryStore' returned %d results after inserting", len(currentMap), len(otherMemoryStoresMap))
|
||||
}
|
||||
|
||||
otherMemoryStore.Clear()
|
||||
currentMap = memoryStore.GetAll()
|
||||
otherMemoryStoresMap = otherMemoryStore.GetAll()
|
||||
|
||||
if len(currentMap) == len(otherMemoryStoresMap) {
|
||||
t.Errorf("Multiple memory stores should refer to different internal maps, but 'memoryStore' returned %d results after clearing, and 'otherMemoryStore' returned %d results after clearing", len(currentMap), len(otherMemoryStoresMap))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_ModificationsToReturnedMapDoNotAffectInternalMap(t *testing.T) {
|
||||
memoryStore.Clear()
|
||||
|
||||
memoryStore.Insert(&testService, &core.Result{})
|
||||
modifiedResults := memoryStore.GetAll()
|
||||
for k := range modifiedResults {
|
||||
delete(modifiedResults, k)
|
||||
}
|
||||
results := memoryStore.GetAll()
|
||||
|
||||
if len(modifiedResults) == len(results) {
|
||||
t.Errorf("Returned map from GetAll should be free to modify by the caller without affecting internal in-memory map, but length of results from in-memory map (%d) was equal to the length of results in modified map (%d)", len(results), len(modifiedResults))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_GetServiceStatusForExistingStatusReturnsThatServiceStatus(t *testing.T) {
|
||||
memoryStore.Clear()
|
||||
|
||||
memoryStore.Insert(&testService, &core.Result{})
|
||||
serviceStatus := memoryStore.GetServiceStatus(testService.Group, testService.Name)
|
||||
func TestInMemoryStore_GetServiceStatus(t *testing.T) {
|
||||
store := NewInMemoryStore()
|
||||
store.Insert(&testService, &testSuccessfulResult)
|
||||
store.Insert(&testService, &testUnsuccessfulResult)
|
||||
|
||||
serviceStatus := store.GetServiceStatus(testService.Group, testService.Name)
|
||||
if serviceStatus == nil {
|
||||
t.Errorf("Returned service status for group '%s' and name '%s' was nil after inserting the service into the store", testService.Group, testService.Name)
|
||||
t.Fatalf("serviceStatus shouldn't have been nil")
|
||||
}
|
||||
if serviceStatus.Uptime == nil {
|
||||
t.Fatalf("serviceStatus.Uptime shouldn't have been nil")
|
||||
}
|
||||
if serviceStatus.Uptime.LastHour != 0.5 {
|
||||
t.Errorf("serviceStatus.Uptime.LastHour should've been 0.5")
|
||||
}
|
||||
if serviceStatus.Uptime.LastTwentyFourHours != 0.5 {
|
||||
t.Errorf("serviceStatus.Uptime.LastTwentyFourHours should've been 0.5")
|
||||
}
|
||||
if serviceStatus.Uptime.LastSevenDays != 0.5 {
|
||||
t.Errorf("serviceStatus.Uptime.LastSevenDays should've been 0.5")
|
||||
}
|
||||
fmt.Println(serviceStatus.Results[0].Timestamp.Format(time.RFC3339))
|
||||
}
|
||||
|
||||
func TestStorage_GetServiceStatusForMissingStatusReturnsNil(t *testing.T) {
|
||||
memoryStore.Clear()
|
||||
func TestInMemoryStore_GetServiceStatusForMissingStatusReturnsNil(t *testing.T) {
|
||||
store := NewInMemoryStore()
|
||||
store.Insert(&testService, &testSuccessfulResult)
|
||||
|
||||
memoryStore.Insert(&testService, &core.Result{})
|
||||
|
||||
serviceStatus := memoryStore.GetServiceStatus("nonexistantgroup", "nonexistantname")
|
||||
serviceStatus := store.GetServiceStatus("nonexistantgroup", "nonexistantname")
|
||||
if serviceStatus != nil {
|
||||
t.Errorf("Returned service status for group '%s' and name '%s' not nil after inserting the service into the store", testService.Group, testService.Name)
|
||||
}
|
||||
|
||||
serviceStatus = memoryStore.GetServiceStatus(testService.Group, "nonexistantname")
|
||||
serviceStatus = store.GetServiceStatus(testService.Group, "nonexistantname")
|
||||
if serviceStatus != nil {
|
||||
t.Errorf("Returned service status for group '%s' and name '%s' not nil after inserting the service into the store", testService.Group, "nonexistantname")
|
||||
}
|
||||
|
||||
serviceStatus = memoryStore.GetServiceStatus("nonexistantgroup", testService.Name)
|
||||
serviceStatus = store.GetServiceStatus("nonexistantgroup", testService.Name)
|
||||
if serviceStatus != nil {
|
||||
t.Errorf("Returned service status for group '%s' and name '%s' not nil after inserting the service into the store", "nonexistantgroup", testService.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInMemoryStore_GetAllAsJSON(t *testing.T) {
|
||||
store := NewInMemoryStore()
|
||||
firstResult := &testSuccessfulResult
|
||||
secondResult := &testUnsuccessfulResult
|
||||
store.Insert(&testService, firstResult)
|
||||
store.Insert(&testService, secondResult)
|
||||
// Can't be bothered dealing with timezone issues on the worker that runs the automated tests
|
||||
firstResult.Timestamp = time.Time{}
|
||||
secondResult.Timestamp = time.Time{}
|
||||
output, err := store.GetAllAsJSON()
|
||||
if err != nil {
|
||||
t.Fatal("shouldn't have returned an error, got", err.Error())
|
||||
}
|
||||
expectedOutput := `{"group_name":{"name":"name","group":"group","results":[{"status":200,"hostname":"example.org","duration":150000000,"errors":null,"condition-results":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"condition-results":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}],"uptime":{"7d":0.5,"24h":0.5,"1h":0.5}}}`
|
||||
if string(output) != expectedOutput {
|
||||
t.Errorf("expected:\n %s\n\ngot:\n %s", expectedOutput, string(output))
|
||||
}
|
||||
}
|
||||
|
||||
163
vendor/github.com/TwinProduction/gocache/README.md
generated
vendored
163
vendor/github.com/TwinProduction/gocache/README.md
generated
vendored
@@ -4,8 +4,7 @@
|
||||
[](https://goreportcard.com/report/github.com/TwinProduction/gocache)
|
||||
[](https://codecov.io/gh/TwinProduction/gocache)
|
||||
[](https://github.com/TwinProduction/gocache)
|
||||
[](https://godoc.org/github.com/TwinProduction/gocache)
|
||||
[](https://cloud.docker.com/repository/docker/twinproduction/gocache-server)
|
||||
[](https://pkg.go.dev/github.com/TwinProduction/gocache)
|
||||
|
||||
gocache is an easy-to-use, high-performance, lightweight and thread-safe (goroutine-safe) in-memory key-value cache
|
||||
with support for LRU and FIFO eviction policies as well as expiration, bulk operations and even persistence to file.
|
||||
@@ -27,6 +26,7 @@ with support for LRU and FIFO eviction policies as well as expiration, bulk oper
|
||||
- [Eviction](#eviction)
|
||||
- [MaxSize](#maxsize)
|
||||
- [MaxMemoryUsage](#maxmemoryusage)
|
||||
- [Expiration](#expiration)
|
||||
- [Server](#server)
|
||||
- [Running the server with Docker](#running-the-server-with-docker)
|
||||
- [Performance](#performance)
|
||||
@@ -71,28 +71,31 @@ cache.StartJanitor()
|
||||
```
|
||||
|
||||
### Functions
|
||||
| Function | Description |
|
||||
| --------------------------------- | ----------- |
|
||||
| WithMaxSize | Sets the max size of the cache. `gocache.NoMaxSize` means there is no limit. If not set, the default max size is `gocache.DefaultMaxSize`.
|
||||
| WithMaxMemoryUsage | Sets the max memory usage of the cache. `gocache.NoMaxMemoryUsage` means there is no limit. The default behavior is to not evict based on memory usage.
|
||||
| WithEvictionPolicy | Sets the eviction algorithm to be used when the cache reaches the max size. If not set, the default eviction policy is `gocache.FirstInFirstOut` (FIFO).
|
||||
| WithForceNilInterfaceOnNilPointer | Configures whether values with a nil pointer passed to write functions should be forcefully set to nil. Defaults to true.
|
||||
| StartJanitor | Starts the janitor, which is in charge of deleting expired cache entries in the background.
|
||||
| StopJanitor | Stops the janitor.
|
||||
| Set | Same as `SetWithTTL`, but with no expiration (`gocache.NoExpiration`)
|
||||
| SetAll | Same as `Set`, but in bulk
|
||||
| SetWithTTL | Creates or updates a cache entry with the given key, value and expiration time. If the max size after the aforementioned operation is above the configured max size, the tail will be evicted. Depending on the eviction policy, the tail is defined as the oldest
|
||||
| Get | Gets a cache entry by its key.
|
||||
| GetByKeys | Gets a map of entries by their keys. The resulting map will contain all keys, even if some of the keys in the slice passed as parameter were not present in the cache.
|
||||
| GetAll | Gets all cache entries.
|
||||
| GetKeysByPattern | Retrieves a slice of keys that matches a given pattern.
|
||||
| Delete | Removes a key from the cache.
|
||||
| DeleteAll | Removes multiple keys from the cache.
|
||||
| Count | Gets the size of the cache. This includes cache keys which may have already expired, but have not been removed yet.
|
||||
| Clear | Wipes the cache.
|
||||
| TTL | Gets the time until a cache key expires.
|
||||
| Expire | Sets the expiration time of an existing cache key.
|
||||
| SaveToFile | Stores the content of the cache to a file so that it can be read using `ReadFromFile`. See [persistence](#persistence).
|
||||
| ReadFromFile | Populates the cache using a file created using `SaveToFile`. See [persistence](#persistence).
|
||||
|
||||
| Function | Description |
|
||||
| ------------------ | ----------- |
|
||||
| WithMaxSize | Sets the max size of the cache. `gocache.NoMaxSize` means there is no limit. If not set, the default max size is `gocache.DefaultMaxSize`.
|
||||
| WithMaxMemoryUsage | Sets the max memory usage of the cache. `gocache.NoMaxMemoryUsage` means there is no limit. The default behavior is to not evict based on memory usage.
|
||||
| WithEvictionPolicy | Sets the eviction algorithm to be used when the cache reaches the max size. If not set, the default eviction policy is `gocache.FirstInFirstOut` (FIFO).
|
||||
| StartJanitor | Starts the janitor, which is in charge of deleting expired cache entries in the background.
|
||||
| StopJanitor | Stops the janitor.
|
||||
| Set | Same as `SetWithTTL`, but with no expiration (`gocache.NoExpiration`)
|
||||
| SetAll | Same as `Set`, but in bulk
|
||||
| SetWithTTL | Creates or updates a cache entry with the given key, value and expiration time. If the max size after the aforementioned operation is above the configured max size, the tail will be evicted. Depending on the eviction policy, the tail is defined as the oldest
|
||||
| Get | Gets a cache entry by its key.
|
||||
| GetAll | Gets a map of entries by their keys. The resulting map will contain all keys, even if some of the keys in the slice passed as parameter were not present in the cache.
|
||||
| GetKeysByPattern | Retrieves a slice of keys that matches a given pattern.
|
||||
| Delete | Removes a key from the cache.
|
||||
| DeleteAll | Removes multiple keys from the cache.
|
||||
| Count | Gets the size of the cache. This includes cache keys which may have already expired, but have not been removed yet.
|
||||
| Clear | Wipes the cache.
|
||||
| TTL | Gets the time until a cache key expires.
|
||||
| Expire | Sets the expiration time of an existing cache key.
|
||||
| SaveToFile | Stores the content of the cache to a file so that it can be read using `ReadFromFile`. See [persistence](#persistence).
|
||||
| ReadFromFile | Populates the cache using a file created using `SaveToFile`. See [persistence](#persistence).
|
||||
For further documentation, please refer to [Go Reference](https://pkg.go.dev/github.com/TwinProduction/gocache)
|
||||
|
||||
|
||||
### Examples
|
||||
@@ -102,13 +105,14 @@ cache.StartJanitor()
|
||||
cache.Set("key", "value")
|
||||
cache.Set("key", 1)
|
||||
cache.Set("key", struct{ Text string }{Test: "value"})
|
||||
cache.SetWithTTL("key", []byte("value"), 24*time.Hour)
|
||||
```
|
||||
|
||||
#### Getting an entry
|
||||
```go
|
||||
value, ok := cache.Get("key")
|
||||
value, exists := cache.Get("key")
|
||||
```
|
||||
You can also get multiple entries by using `cache.GetAll([]string{"key1", "key2"})`
|
||||
You can also get multiple entries by using `cache.GetByKeys([]string{"key1", "key2"})`
|
||||
|
||||
#### Deleting an entry
|
||||
```go
|
||||
@@ -122,8 +126,9 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/TwinProduction/gocache"
|
||||
"time"
|
||||
|
||||
"github.com/TwinProduction/gocache"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -136,8 +141,8 @@ func main() {
|
||||
|
||||
value, exists := cache.Get("key")
|
||||
fmt.Printf("[Get] key=key; value=%s; exists=%v\n", value, exists)
|
||||
for key, value := range cache.GetAll([]string{"k1", "k2", "k3"}) {
|
||||
fmt.Printf("[GetAll] key=%s; value=%s\n", key, value)
|
||||
for key, value := range cache.GetByKeys([]string{"k1", "k2", "k3"}) {
|
||||
fmt.Printf("[GetByKeys] key=%s; value=%s\n", key, value)
|
||||
}
|
||||
for _, key := range cache.GetKeysByPattern("key*", 0) {
|
||||
fmt.Printf("[GetKeysByPattern] key=%s\n", key)
|
||||
@@ -174,9 +179,9 @@ func main() {
|
||||
|
||||
```
|
||||
[Get] key=key; value=value; exists=true
|
||||
[GetAll] key=k2; value=v2
|
||||
[GetAll] key=k3; value=v3
|
||||
[GetAll] key=k1; value=v1
|
||||
[GetByKeys] key=k2; value=v2
|
||||
[GetByKeys] key=k3; value=v3
|
||||
[GetByKeys] key=k1; value=v1
|
||||
[GetKeysByPattern] key=key
|
||||
[GetKeysByPattern] key=key-with-ttl
|
||||
Cache size before persisting cache to file: 5
|
||||
@@ -248,6 +253,7 @@ you'll be fine.
|
||||
|
||||
|
||||
## Eviction
|
||||
|
||||
### MaxSize
|
||||
Eviction by MaxSize is the default behavior, and is also the most efficient.
|
||||
|
||||
@@ -258,7 +264,7 @@ cache := gocache.NewCache().WithMaxSize(1000)
|
||||
This means that whenever an operation causes the total size of the cache to go above 1000, the tail will be evicted.
|
||||
|
||||
### MaxMemoryUsage
|
||||
Eviction by MaxMemoryUsage is **disabled by default**, and is still a work in progress.
|
||||
Eviction by MaxMemoryUsage is **disabled by default**, and is in alpha.
|
||||
|
||||
The code below will create a cache that has a maximum memory usage of 50MB:
|
||||
```go
|
||||
@@ -268,7 +274,7 @@ This means that whenever an operation causes the total memory usage of the cache
|
||||
will be evicted.
|
||||
|
||||
Unlike evictions caused by reaching the MaxSize, evictions triggered by MaxMemoryUsage may lead to multiple entries
|
||||
being evicted in a row. The reason for this is that if, for instance, you had 500 entries of 0.1MB each and you suddenly added
|
||||
being evicted in a row. The reason for this is that if, for instance, you had 100 entries of 0.1MB each and you suddenly added
|
||||
a single entry of 10MB, 100 entries would need to be evicted to make enough space for that new big entry.
|
||||
|
||||
It's very important to keep in mind that eviction by MaxMemoryUsage is approximate.
|
||||
@@ -284,6 +290,18 @@ As previously mentioned, this is a work in progress, and here's a list of the th
|
||||
- Adding an entry bigger than the configured MaxMemoryUsage will work, but it will evict all other entries.
|
||||
|
||||
|
||||
## Expiration
|
||||
There are two ways that the deletion of expired keys can take place:
|
||||
- Active
|
||||
- Passive
|
||||
|
||||
**Active deletion of expired keys** happens when an attempt is made to access the value of a cache entry that expired.
|
||||
`Get`, `GetByKeys` and `GetAll` are the only functions that can trigger active deletion of expired keys.
|
||||
|
||||
**Passive deletion of expired keys** runs in the background and is managed by the janitor.
|
||||
If you do not start the janitor, there will be no passive deletion of expired keys.
|
||||
|
||||
|
||||
## Server
|
||||
For the sake of convenience, a ready-to-go cache server is available
|
||||
through the `gocacheserver` package.
|
||||
@@ -330,12 +348,14 @@ Any Redis client should be able to interact with the server, though only the fol
|
||||
|
||||
|
||||
## Running the server with Docker
|
||||
[](https://cloud.docker.com/repository/docker/twinproduction/gocache-server)
|
||||
|
||||
To build it locally, refer to the Makefile's `docker-build` and `docker-run` steps.
|
||||
|
||||
Note that the server version of gocache is still under development.
|
||||
|
||||
```
|
||||
docker run --name gocache-server -p 6379:6379 twinproduction/gocache-server:v0.1.0
|
||||
docker run --name gocache-server -p 6379:6379 twinproduction/gocache-server
|
||||
```
|
||||
|
||||
|
||||
@@ -362,45 +382,52 @@ but if you're looking into using a library like gocache, odds are, you want more
|
||||
| mem | 32G DDR4 |
|
||||
|
||||
```
|
||||
BenchmarkMap_Get-8 47943618 26.6 ns/op
|
||||
BenchmarkMap_SetSmallValue-8 3800810 394 ns/op
|
||||
BenchmarkMap_SetMediumValue-8 3904794 400 ns/op
|
||||
BenchmarkMap_SetLargeValue-8 3934033 383 ns/op
|
||||
BenchmarkCache_Get-8 27254640 45.0 ns/op
|
||||
BenchmarkCache_SetSmallValue-8 2991620 401 ns/op
|
||||
BenchmarkCache_SetMediumValue-8 3051128 381 ns/op
|
||||
BenchmarkCache_SetLargeValue-8 2995904 382 ns/op
|
||||
BenchmarkCache_SetSmallValueWhenUsingMaxMemoryUsage-8 2752288 428 ns/op
|
||||
BenchmarkCache_SetMediumValueWhenUsingMaxMemoryUsage-8 2744899 436 ns/op
|
||||
BenchmarkCache_SetLargeValueWhenUsingMaxMemoryUsage-8 2756816 430 ns/op
|
||||
BenchmarkCache_SetSmallValueWithMaxSize10-8 5308886 226 ns/op
|
||||
BenchmarkCache_SetMediumValueWithMaxSize10-8 5304098 226 ns/op
|
||||
BenchmarkCache_SetLargeValueWithMaxSize10-8 5277986 227 ns/op
|
||||
BenchmarkCache_SetSmallValueWithMaxSize1000-8 5130580 236 ns/op
|
||||
BenchmarkCache_SetMediumValueWithMaxSize1000-8 5102404 237 ns/op
|
||||
BenchmarkCache_SetLargeValueWithMaxSize1000-8 5084695 237 ns/op
|
||||
BenchmarkCache_SetSmallValueWithMaxSize100000-8 3858066 315 ns/op
|
||||
BenchmarkCache_SetMediumValueWithMaxSize100000-8 3909277 315 ns/op
|
||||
BenchmarkCache_SetLargeValueWithMaxSize100000-8 3870913 315 ns/op
|
||||
BenchmarkCache_SetSmallValueWithMaxSize100000AndLRU-8 3856012 316 ns/op
|
||||
BenchmarkCache_SetMediumValueWithMaxSize100000AndLRU-8 3809518 316 ns/op
|
||||
BenchmarkCache_SetLargeValueWithMaxSize100000AndLRU-8 3834754 318 ns/op
|
||||
BenchmarkCache_GetAndSetConcurrently-8 1779258 672 ns/op
|
||||
BenchmarkCache_GetAndSetConcurrentlyWithRandomKeysAndLRU-8 2569590 487 ns/op
|
||||
BenchmarkCache_GetAndSetConcurrentlyWithRandomKeysAndFIFO-8 2608369 474 ns/op
|
||||
BenchmarkCache_GetAndSetConcurrentlyWithRandomKeysAndNoEvictionAndLRU-8 2185795 582 ns/op
|
||||
BenchmarkCache_GetAndSetConcurrentlyWithRandomKeysAndNoEvictionAndFIFO-8 2238811 568 ns/op
|
||||
BenchmarkCache_GetAndSetConcurrentlyWithFrequentEvictionsAndLRU-8 3726714 320 ns/op
|
||||
BenchmarkCache_GetAndSetConcurrentlyWithFrequentEvictionsAndFIFO-8 3682808 325 ns/op
|
||||
BenchmarkCache_GetConcurrentlyWithLRU-8 1536589 739 ns/op
|
||||
BenchmarkCache_GetConcurrentlyWithFIFO-8 1558513 737 ns/op
|
||||
BenchmarkCache_GetKeysThatDoNotExistConcurrently-8 10173138 119 ns/op
|
||||
BenchmarkMap_Get-8 95936680 26.3 ns/op
|
||||
BenchmarkMap_SetSmallValue-8 7738132 424 ns/op
|
||||
BenchmarkMap_SetMediumValue-8 7766346 424 ns/op
|
||||
BenchmarkMap_SetLargeValue-8 7947063 435 ns/op
|
||||
BenchmarkCache_Get-8 54549049 45.7 ns/op
|
||||
BenchmarkCache_SetSmallValue-8 35225013 69.2 ns/op
|
||||
BenchmarkCache_SetMediumValue-8 5952064 412 ns/op
|
||||
BenchmarkCache_SetLargeValue-8 5969121 411 ns/op
|
||||
BenchmarkCache_GetUsingLRU-8 54545949 45.6 ns/op
|
||||
BenchmarkCache_SetSmallValueUsingLRU-8 5909504 419 ns/op
|
||||
BenchmarkCache_SetMediumValueUsingLRU-8 5910885 418 ns/op
|
||||
BenchmarkCache_SetLargeValueUsingLRU-8 5867544 419 ns/op
|
||||
BenchmarkCache_SetSmallValueWhenUsingMaxMemoryUsage-8 5477178 462 ns/op
|
||||
BenchmarkCache_SetMediumValueWhenUsingMaxMemoryUsage-8 5417595 475 ns/op
|
||||
BenchmarkCache_SetLargeValueWhenUsingMaxMemoryUsage-8 5215263 479 ns/op
|
||||
BenchmarkCache_SetSmallValueWithMaxSize10-8 10115574 236 ns/op
|
||||
BenchmarkCache_SetMediumValueWithMaxSize10-8 10242792 241 ns/op
|
||||
BenchmarkCache_SetLargeValueWithMaxSize10-8 10201894 241 ns/op
|
||||
BenchmarkCache_SetSmallValueWithMaxSize1000-8 9637113 253 ns/op
|
||||
BenchmarkCache_SetMediumValueWithMaxSize1000-8 9635175 253 ns/op
|
||||
BenchmarkCache_SetLargeValueWithMaxSize1000-8 9598982 260 ns/op
|
||||
BenchmarkCache_SetSmallValueWithMaxSize100000-8 7642584 337 ns/op
|
||||
BenchmarkCache_SetMediumValueWithMaxSize100000-8 7407571 344 ns/op
|
||||
BenchmarkCache_SetLargeValueWithMaxSize100000-8 7071360 345 ns/op
|
||||
BenchmarkCache_SetSmallValueWithMaxSize100000AndLRU-8 7544194 332 ns/op
|
||||
BenchmarkCache_SetMediumValueWithMaxSize100000AndLRU-8 7667004 344 ns/op
|
||||
BenchmarkCache_SetLargeValueWithMaxSize100000AndLRU-8 7357642 338 ns/op
|
||||
BenchmarkCache_GetAndSetMultipleConcurrently-8 1442306 1684 ns/op
|
||||
BenchmarkCache_GetAndSetConcurrentlyWithRandomKeysAndLRU-8 5117271 477 ns/op
|
||||
BenchmarkCache_GetAndSetConcurrentlyWithRandomKeysAndFIFO-8 5228412 475 ns/op
|
||||
BenchmarkCache_GetAndSetConcurrentlyWithRandomKeysAndNoEvictionAndLRU-8 5139195 529 ns/op
|
||||
BenchmarkCache_GetAndSetConcurrentlyWithRandomKeysAndNoEvictionAndFIFO-8 5251639 511 ns/op
|
||||
BenchmarkCache_GetAndSetConcurrentlyWithFrequentEvictionsAndLRU-8 7384626 334 ns/op
|
||||
BenchmarkCache_GetAndSetConcurrentlyWithFrequentEvictionsAndFIFO-8 7361985 332 ns/op
|
||||
BenchmarkCache_GetConcurrentlyWithLRU-8 3370784 726 ns/op
|
||||
BenchmarkCache_GetConcurrentlyWithFIFO-8 3749994 681 ns/op
|
||||
BenchmarkCache_GetKeysThatDoNotExistConcurrently-8 17647344 143 ns/op
|
||||
```
|
||||
|
||||
|
||||
## FAQ
|
||||
|
||||
### Why does the memory usage not go down?
|
||||
|
||||
> **NOTE**: As of Go 1.16, this will no longer apply. See [golang/go#42330](https://github.com/golang/go/issues/42330)
|
||||
|
||||
By default, Go uses `MADV_FREE` if the kernel supports it to release memory, which is significantly more efficient
|
||||
than using `MADV_DONTNEED`. Unfortunately, this means that RSS doesn't go down unless the OS actually needs the
|
||||
memory.
|
||||
@@ -422,4 +449,4 @@ You can reproduce this by following the steps below:
|
||||
**Substituting gocache for a normal map will yield the same result.**
|
||||
|
||||
If the released memory still appearing as used is a problem for you,
|
||||
you can set the environment variable `GODEBUG` to `madvdontneed=1`.
|
||||
you can set the environment variable `GODEBUG` to `madvdontneed=1`.
|
||||
|
||||
9
vendor/github.com/TwinProduction/gocache/entry.go
generated
vendored
9
vendor/github.com/TwinProduction/gocache/entry.go
generated
vendored
@@ -6,8 +6,12 @@ import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Entry is a cache entry
|
||||
type Entry struct {
|
||||
Key string
|
||||
// Key is the name of the cache entry
|
||||
Key string
|
||||
|
||||
// Value is the value of the cache entry
|
||||
Value interface{}
|
||||
|
||||
// RelevantTimestamp is the variable used to store either:
|
||||
@@ -24,10 +28,12 @@ type Entry struct {
|
||||
previous *Entry
|
||||
}
|
||||
|
||||
// Accessed updates the Entry's RelevantTimestamp to now
|
||||
func (entry *Entry) Accessed() {
|
||||
entry.RelevantTimestamp = time.Now()
|
||||
}
|
||||
|
||||
// Expired returns whether the Entry has expired
|
||||
func (entry Entry) Expired() bool {
|
||||
if entry.Expiration > 0 {
|
||||
if time.Now().UnixNano() > entry.Expiration {
|
||||
@@ -37,6 +43,7 @@ func (entry Entry) Expired() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// SizeInBytes returns the size of an entry in bytes, approximately.
|
||||
func (entry *Entry) SizeInBytes() int {
|
||||
return toBytes(entry.Key) + toBytes(entry.Value) + 32
|
||||
}
|
||||
|
||||
175
vendor/github.com/TwinProduction/gocache/gocache.go
generated
vendored
175
vendor/github.com/TwinProduction/gocache/gocache.go
generated
vendored
@@ -2,13 +2,16 @@ package gocache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
var (
|
||||
Debug = false
|
||||
)
|
||||
|
||||
const (
|
||||
// NoMaxSize means that the cache has no maximum number of entries in the cache
|
||||
// Setting Cache.maxSize to this value also means there will be no eviction
|
||||
NoMaxSize = 0
|
||||
@@ -23,15 +26,14 @@ const (
|
||||
NoExpiration = -1
|
||||
|
||||
Kilobyte = 1024
|
||||
Megabyte = 1024 * 1024
|
||||
Gigabyte = 1024 * 1024 * 1024
|
||||
Megabyte = 1024 * Kilobyte
|
||||
Gigabyte = 1024 * Megabyte
|
||||
)
|
||||
|
||||
var (
|
||||
ErrKeyDoesNotExist = errors.New("key does not exist")
|
||||
ErrKeyHasNoExpiration = errors.New("key has no expiration")
|
||||
ErrJanitorAlreadyRunning = errors.New("janitor is already running")
|
||||
ErrAutoSaveAlreadyRunning = errors.New("autosave is already running")
|
||||
ErrKeyDoesNotExist = errors.New("key does not exist")
|
||||
ErrKeyHasNoExpiration = errors.New("key has no expiration")
|
||||
ErrJanitorAlreadyRunning = errors.New("janitor is already running")
|
||||
)
|
||||
|
||||
// Cache is the core struct of gocache which contains the data as well as all relevant configuration fields
|
||||
@@ -68,6 +70,15 @@ type Cache struct {
|
||||
|
||||
// memoryUsage is the approximate memory usage of the cache (dataset only) in bytes
|
||||
memoryUsage int
|
||||
|
||||
// forceNilInterfaceOnNilPointer determines whether all Set-like functions should set a value as nil if the
|
||||
// interface passed has a nil value but not a nil type.
|
||||
//
|
||||
// By default, interfaces are only nil when both their type and value is nil.
|
||||
// This means that when you pass a pointer to a nil value, the type of the interface
|
||||
// will still show as nil, which means that if you don't cast the interface after
|
||||
// retrieving it, a nil check will return that the value is not false.
|
||||
forceNilInterfaceOnNilPointer bool
|
||||
}
|
||||
|
||||
// MaxSize returns the maximum amount of keys that can be present in the cache before
|
||||
@@ -87,8 +98,16 @@ func (cache *Cache) EvictionPolicy() EvictionPolicy {
|
||||
}
|
||||
|
||||
// Stats returns statistics from the cache
|
||||
func (cache *Cache) Stats() *Statistics {
|
||||
return cache.stats
|
||||
func (cache *Cache) Stats() Statistics {
|
||||
cache.mutex.RLock()
|
||||
stats := Statistics{
|
||||
EvictedKeys: cache.stats.EvictedKeys,
|
||||
ExpiredKeys: cache.stats.ExpiredKeys,
|
||||
Hits: cache.stats.Hits,
|
||||
Misses: cache.stats.Misses,
|
||||
}
|
||||
cache.mutex.RUnlock()
|
||||
return stats
|
||||
}
|
||||
|
||||
// MemoryUsage returns the current memory usage of the cache's dataset in bytes
|
||||
@@ -103,6 +122,9 @@ func (cache *Cache) WithMaxSize(maxSize int) *Cache {
|
||||
if maxSize < 0 {
|
||||
maxSize = NoMaxSize
|
||||
}
|
||||
if maxSize != NoMaxSize && cache.Count() == 0 {
|
||||
cache.entries = make(map[string]*Entry, maxSize)
|
||||
}
|
||||
cache.maxSize = maxSize
|
||||
return cache
|
||||
}
|
||||
@@ -127,20 +149,62 @@ func (cache *Cache) WithEvictionPolicy(policy EvictionPolicy) *Cache {
|
||||
return cache
|
||||
}
|
||||
|
||||
// WithForceNilInterfaceOnNilPointer sets whether all Set-like functions should set a value as nil if the
|
||||
// interface passed has a nil value but not a nil type.
|
||||
//
|
||||
// In Go, an interface is only nil if both its type and value are nil, which means that a nil pointer
|
||||
// (e.g. (*Struct)(nil)) will retain its attribution to the type, and the unmodified value returned from
|
||||
// Cache.Get, for instance, would return false when compared with nil if this option is set to false.
|
||||
//
|
||||
// We can bypass this by detecting if the interface's value is nil and setting it to nil rather than
|
||||
// a nil pointer, which will make the value returned from Cache.Get return true when compared with nil.
|
||||
// This is exactly what passing true to WithForceNilInterfaceOnNilPointer does, and it's also the default behavior.
|
||||
//
|
||||
// Alternatively, you may pass false to WithForceNilInterfaceOnNilPointer, which will mean that you'll have
|
||||
// to cast the value returned from Cache.Get to its original type to check for whether the pointer returned
|
||||
// is nil or not.
|
||||
//
|
||||
// If set to true:
|
||||
// cache := gocache.NewCache().WithForceNilInterfaceOnNilPointer(true)
|
||||
// cache.Set("key", (*Struct)(nil))
|
||||
// value, _ := cache.Get("key")
|
||||
// // the following returns true, because the interface{} was forcefully set to nil
|
||||
// if value == nil {}
|
||||
// // the following will panic, because the value has been casted to its type
|
||||
// if value.(*Struct) == nil {}
|
||||
//
|
||||
// If set to false:
|
||||
// cache := gocache.NewCache().WithForceNilInterfaceOnNilPointer(false)
|
||||
// cache.Set("key", (*Struct)(nil))
|
||||
// value, _ := cache.Get("key")
|
||||
// // the following returns false, because the interface{} returned has a non-nil type (*Struct)
|
||||
// if value == nil {}
|
||||
// // the following returns true, because the value has been casted to its type
|
||||
// if value.(*Struct) == nil {}
|
||||
//
|
||||
// In other words, if set to true, you do not need to cast the value returned from the cache to
|
||||
// to check if the value is nil.
|
||||
//
|
||||
// Defaults to true
|
||||
func (cache *Cache) WithForceNilInterfaceOnNilPointer(forceNilInterfaceOnNilPointer bool) *Cache {
|
||||
cache.forceNilInterfaceOnNilPointer = forceNilInterfaceOnNilPointer
|
||||
return cache
|
||||
}
|
||||
|
||||
// NewCache creates a new Cache
|
||||
//
|
||||
// Should be used in conjunction with Cache.WithMaxSize, Cache.WithMaxMemoryUsage and/or Cache.WithEvictionPolicy
|
||||
//
|
||||
// gocache.NewCache().WithMaxSize(10000).WithEvictionPolicy(gocache.LeastRecentlyUsed)
|
||||
//
|
||||
func NewCache() *Cache {
|
||||
return &Cache{
|
||||
maxSize: DefaultMaxSize,
|
||||
evictionPolicy: FirstInFirstOut,
|
||||
stats: &Statistics{},
|
||||
entries: make(map[string]*Entry),
|
||||
mutex: sync.RWMutex{},
|
||||
stopJanitor: nil,
|
||||
maxSize: DefaultMaxSize,
|
||||
evictionPolicy: FirstInFirstOut,
|
||||
stats: &Statistics{},
|
||||
entries: make(map[string]*Entry),
|
||||
mutex: sync.RWMutex{},
|
||||
stopJanitor: nil,
|
||||
forceNilInterfaceOnNilPointer: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,13 +214,22 @@ func (cache *Cache) Set(key string, value interface{}) {
|
||||
}
|
||||
|
||||
// SetWithTTL creates or updates a key with a given value and sets an expiration time (-1 is NoExpiration)
|
||||
//
|
||||
// The TTL provided must be greater than 0, or NoExpiration (-1). If a negative value that isn't -1 (NoExpiration) is
|
||||
// provided, the entry will not be created if the key doesn't exist
|
||||
func (cache *Cache) SetWithTTL(key string, value interface{}, ttl time.Duration) {
|
||||
// An interface is only nil if both its value and its type are nil, however, passing a pointer
|
||||
if cache.forceNilInterfaceOnNilPointer {
|
||||
if value != nil && (reflect.ValueOf(value).Kind() == reflect.Ptr && reflect.ValueOf(value).IsNil()) {
|
||||
value = nil
|
||||
}
|
||||
}
|
||||
cache.mutex.Lock()
|
||||
entry, ok := cache.get(key)
|
||||
if !ok {
|
||||
// A negative TTL that isn't -1 (NoExpiration) is an entry that will expire instantly,
|
||||
// A negative TTL that isn't -1 (NoExpiration) or 0 is an entry that will expire instantly,
|
||||
// so might as well just not create it in the first place
|
||||
if ttl != NoExpiration && ttl < 0 {
|
||||
if ttl != NoExpiration && ttl < 1 {
|
||||
cache.mutex.Unlock()
|
||||
return
|
||||
}
|
||||
@@ -178,6 +251,13 @@ func (cache *Cache) SetWithTTL(key string, value interface{}, ttl time.Duration)
|
||||
cache.memoryUsage += entry.SizeInBytes()
|
||||
}
|
||||
} else {
|
||||
// A negative TTL that isn't -1 (NoExpiration) or 0 is an entry that will expire instantly,
|
||||
// so might as well just delete it immediately instead of updating it
|
||||
if ttl != NoExpiration && ttl < 1 {
|
||||
cache.delete(key)
|
||||
cache.mutex.Unlock()
|
||||
return
|
||||
}
|
||||
if cache.maxMemoryUsage != NoMaxMemoryUsage {
|
||||
// Substract the old entry from the cache's memoryUsage
|
||||
cache.memoryUsage -= entry.SizeInBytes()
|
||||
@@ -234,12 +314,13 @@ func (cache *Cache) Get(key string) (interface{}, bool) {
|
||||
cache.stats.Misses++
|
||||
return nil, false
|
||||
}
|
||||
cache.stats.Hits++
|
||||
if entry.Expired() {
|
||||
cache.stats.ExpiredKeys++
|
||||
cache.delete(key)
|
||||
cache.mutex.Unlock()
|
||||
return nil, false
|
||||
}
|
||||
cache.stats.Hits++
|
||||
if cache.evictionPolicy == LeastRecentlyUsed {
|
||||
entry.Accessed()
|
||||
if cache.head == entry {
|
||||
@@ -253,12 +334,11 @@ func (cache *Cache) Get(key string) (interface{}, bool) {
|
||||
return entry.Value, true
|
||||
}
|
||||
|
||||
// GetAll retrieves multiple entries using the keys passed as parameter
|
||||
// All keys are returned in the map, regardless of whether they exist or not,
|
||||
// however, entries that do not exist in the cache will return nil, meaning that
|
||||
// there is no way of determining whether a key genuinely has the value nil, or
|
||||
// whether it doesn't exist in the cache using only this function
|
||||
func (cache *Cache) GetAll(keys []string) map[string]interface{} {
|
||||
// GetByKeys retrieves multiple entries using the keys passed as parameter
|
||||
// All keys are returned in the map, regardless of whether they exist or not, however, entries that do not exist in the
|
||||
// cache will return nil, meaning that there is no way of determining whether a key genuinely has the value nil, or
|
||||
// whether it doesn't exist in the cache using only this function.
|
||||
func (cache *Cache) GetByKeys(keys []string) map[string]interface{} {
|
||||
entries := make(map[string]interface{})
|
||||
for _, key := range keys {
|
||||
entries[key], _ = cache.Get(key)
|
||||
@@ -266,18 +346,51 @@ func (cache *Cache) GetAll(keys []string) map[string]interface{} {
|
||||
return entries
|
||||
}
|
||||
|
||||
// GetAll retrieves all cache entries
|
||||
//
|
||||
// If the eviction policy is LeastRecentlyUsed, note that unlike Get and GetByKeys, this does not update the last access
|
||||
// timestamp. The reason for this is that since all cache entries will be accessed, updating the last access timestamp
|
||||
// would provide very little benefit while harming the ability to accurately determine the next key that will be evicted
|
||||
//
|
||||
// You should probably avoid using this if you have a lot of entries.
|
||||
//
|
||||
// GetKeysByPattern is a good alternative if you want to retrieve entries that you do not have the key for, as it only
|
||||
// retrieves the keys and does not trigger active eviction and has a parameter for setting a limit to the number of keys
|
||||
// you wish to retrieve.
|
||||
func (cache *Cache) GetAll() map[string]interface{} {
|
||||
entries := make(map[string]interface{})
|
||||
cache.mutex.Lock()
|
||||
for key, entry := range cache.entries {
|
||||
if entry.Expired() {
|
||||
cache.delete(key)
|
||||
continue
|
||||
}
|
||||
entries[key] = entry.Value
|
||||
}
|
||||
cache.stats.Hits += uint64(len(entries))
|
||||
cache.mutex.Unlock()
|
||||
return entries
|
||||
}
|
||||
|
||||
// GetKeysByPattern retrieves a slice of keys that match a given pattern
|
||||
// If the limit is set to 0, the entire cache will be searched for matching keys.
|
||||
// If the limit is above 0, the search will stop once the specified number of matching keys have been found.
|
||||
//
|
||||
// e.g. cache.GetKeysByPattern("*some*", 0) will return all keys containing "some" in them
|
||||
// e.g. cache.GetKeysByPattern("*some*", 5) will return 5 keys (or less) containing "some" in them
|
||||
// e.g.
|
||||
// cache.GetKeysByPattern("*some*", 0) will return all keys containing "some" in them
|
||||
// cache.GetKeysByPattern("*some*", 5) will return 5 keys (or less) containing "some" in them
|
||||
//
|
||||
// Note that GetKeysByPattern does not trigger evictions, nor does it count as accessing the entry.
|
||||
// Note that GetKeysByPattern does not trigger active evictions, nor does it count as accessing the entry, the latter
|
||||
// only applying if the cache uses the LeastRecentlyUsed eviction policy.
|
||||
// The reason for that behavior is that these two (active eviction and access) only applies when you access the value
|
||||
// of the cache entry, and this function only returns the keys.
|
||||
func (cache *Cache) GetKeysByPattern(pattern string, limit int) []string {
|
||||
var matchingKeys []string
|
||||
cache.mutex.RLock()
|
||||
for key := range cache.entries {
|
||||
cache.mutex.Lock()
|
||||
for key, value := range cache.entries {
|
||||
if value.Expired() {
|
||||
continue
|
||||
}
|
||||
if MatchPattern(pattern, key) {
|
||||
matchingKeys = append(matchingKeys, key)
|
||||
if limit > 0 && len(matchingKeys) >= limit {
|
||||
@@ -285,7 +398,7 @@ func (cache *Cache) GetKeysByPattern(pattern string, limit int) []string {
|
||||
}
|
||||
}
|
||||
}
|
||||
cache.mutex.RUnlock()
|
||||
cache.mutex.Unlock()
|
||||
return matchingKeys
|
||||
}
|
||||
|
||||
|
||||
38
vendor/github.com/TwinProduction/gocache/janitor.go
generated
vendored
38
vendor/github.com/TwinProduction/gocache/janitor.go
generated
vendored
@@ -2,7 +2,6 @@ package gocache
|
||||
|
||||
import (
|
||||
"log"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -24,9 +23,10 @@ const (
|
||||
)
|
||||
|
||||
// StartJanitor starts the janitor on a different goroutine
|
||||
// The janitor's job is to delete expired keys in the background.
|
||||
// The janitor's job is to delete expired keys in the background, in other words, it takes care of passive eviction.
|
||||
// It can be stopped by calling Cache.StopJanitor.
|
||||
// If you do not start the janitor, expired keys will only be deleted when they are accessed through Get
|
||||
// If you do not start the janitor, expired keys will only be deleted when they are accessed through Get, GetByKeys, or
|
||||
// GetAll.
|
||||
func (cache *Cache) StartJanitor() error {
|
||||
if cache.stopJanitor != nil {
|
||||
return ErrJanitorAlreadyRunning
|
||||
@@ -109,26 +109,32 @@ func (cache *Cache) StartJanitor() error {
|
||||
}
|
||||
cache.mutex.Unlock()
|
||||
case <-cache.stopJanitor:
|
||||
cache.stopJanitor = nil
|
||||
cache.stopJanitor <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
if Debug {
|
||||
go func() {
|
||||
var m runtime.MemStats
|
||||
for {
|
||||
runtime.ReadMemStats(&m)
|
||||
log.Printf("Alloc=%vMB; HeapReleased=%vMB; Sys=%vMB; HeapInUse=%vMB; HeapObjects=%v; HeapObjectsFreed=%v; GC=%v; cache.memoryUsage=%vMB; cacheSize=%d\n", m.Alloc/1024/1024, m.HeapReleased/1024/1024, m.Sys/1024/1024, m.HeapInuse/1024/1024, m.HeapObjects, m.Frees, m.NumGC, cache.memoryUsage/1024/1024, cache.Count())
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
}()
|
||||
}
|
||||
//if Debug {
|
||||
// go func() {
|
||||
// var m runtime.MemStats
|
||||
// for {
|
||||
// runtime.ReadMemStats(&m)
|
||||
// log.Printf("Alloc=%vMB; HeapReleased=%vMB; Sys=%vMB; HeapInUse=%vMB; HeapObjects=%v; HeapObjectsFreed=%v; GC=%v; cache.memoryUsage=%vMB; cacheSize=%d\n", m.Alloc/1024/1024, m.HeapReleased/1024/1024, m.Sys/1024/1024, m.HeapInuse/1024/1024, m.HeapObjects, m.Frees, m.NumGC, cache.memoryUsage/1024/1024, cache.Count())
|
||||
// time.Sleep(3 * time.Second)
|
||||
// }
|
||||
// }()
|
||||
//}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopJanitor stops the janitor
|
||||
func (cache *Cache) StopJanitor() {
|
||||
cache.stopJanitor <- true
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if cache.stopJanitor != nil {
|
||||
// Tell the janitor to stop, and then wait for the janitor to reply on the same channel that it's stopping
|
||||
// This may seem a bit odd, but this allows us to avoid a data race condition in which setting cache.stopJanitor
|
||||
// to nil
|
||||
cache.stopJanitor <- true
|
||||
<-cache.stopJanitor
|
||||
cache.stopJanitor = nil
|
||||
}
|
||||
}
|
||||
|
||||
3
vendor/github.com/TwinProduction/gocache/persistence.go
generated
vendored
3
vendor/github.com/TwinProduction/gocache/persistence.go
generated
vendored
@@ -3,11 +3,12 @@ package gocache
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"github.com/boltdb/bolt"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
// SaveToFile stores the content of the cache to a file so that it can be read using
|
||||
|
||||
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
@@ -1,7 +1,7 @@
|
||||
# cloud.google.com/go v0.74.0
|
||||
## explicit
|
||||
cloud.google.com/go/compute/metadata
|
||||
# github.com/TwinProduction/gocache v0.3.0
|
||||
# github.com/TwinProduction/gocache v1.1.0
|
||||
## explicit
|
||||
github.com/TwinProduction/gocache
|
||||
# github.com/beorn7/perks v1.0.1
|
||||
|
||||
@@ -26,30 +26,28 @@ func handleAlertsToTrigger(service *core.Service, result *core.Result, cfg *conf
|
||||
service.NumberOfFailuresInARow++
|
||||
for _, alert := range service.Alerts {
|
||||
// If the alert hasn't been triggered, move to the next one
|
||||
if !alert.Enabled || alert.FailureThreshold != service.NumberOfFailuresInARow {
|
||||
if !alert.Enabled || alert.FailureThreshold > service.NumberOfFailuresInARow {
|
||||
continue
|
||||
}
|
||||
if alert.Triggered {
|
||||
if cfg.Debug {
|
||||
log.Printf("[watchdog][handleAlertsToTrigger] Alert with description='%s' has already been TRIGGERED, skipping", alert.Description)
|
||||
log.Printf("[watchdog][handleAlertsToTrigger] Alert for service=%s with description='%s' has already been TRIGGERED, skipping", service.Name, alert.Description)
|
||||
}
|
||||
continue
|
||||
}
|
||||
alertProvider := config.GetAlertingProviderByAlertType(cfg, alert.Type)
|
||||
if alertProvider != nil && alertProvider.IsValid() {
|
||||
log.Printf("[watchdog][handleAlertsToTrigger] Sending %s alert because alert with description='%s' has been TRIGGERED", alert.Type, alert.Description)
|
||||
log.Printf("[watchdog][handleAlertsToTrigger] Sending %s alert because alert for service=%s with description='%s' has been TRIGGERED", alert.Type, service.Name, alert.Description)
|
||||
customAlertProvider := alertProvider.ToCustomAlertProvider(service, alert, result, false)
|
||||
// TODO: retry on error
|
||||
var err error
|
||||
// We need to extract the DedupKey from PagerDuty's response
|
||||
if alert.Type == core.PagerDutyAlert {
|
||||
var body []byte
|
||||
body, err = customAlertProvider.Send(service.Name, alert.Description, false)
|
||||
if err == nil {
|
||||
if body, err = customAlertProvider.Send(service.Name, alert.Description, false); err == nil {
|
||||
var response pagerDutyResponse
|
||||
err = json.Unmarshal(body, &response)
|
||||
if err != nil {
|
||||
log.Printf("[watchdog][handleAlertsToTrigger] Ran into error unmarshaling pager duty response: %s", err.Error())
|
||||
if err = json.Unmarshal(body, &response); err != nil {
|
||||
log.Printf("[watchdog][handleAlertsToTrigger] Ran into error unmarshaling pagerduty response: %s", err.Error())
|
||||
} else {
|
||||
alert.ResolveKey = response.DedupKey
|
||||
}
|
||||
@@ -59,11 +57,10 @@ func handleAlertsToTrigger(service *core.Service, result *core.Result, cfg *conf
|
||||
_, err = customAlertProvider.Send(service.Name, alert.Description, false)
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("[watchdog][handleAlertsToTrigger] Ran into error sending an alert: %s", err.Error())
|
||||
log.Printf("[watchdog][handleAlertsToTrigger] Failed to send an alert for service=%s: %s", service.Name, err.Error())
|
||||
} else {
|
||||
alert.Triggered = true
|
||||
}
|
||||
|
||||
} else {
|
||||
log.Printf("[watchdog][handleAlertsToResolve] Not sending alert of type=%s despite being TRIGGERED, because the provider wasn't configured properly", alert.Type)
|
||||
}
|
||||
@@ -76,18 +73,20 @@ func handleAlertsToResolve(service *core.Service, result *core.Result, cfg *conf
|
||||
if !alert.Enabled || !alert.Triggered || alert.SuccessThreshold > service.NumberOfSuccessesInARow {
|
||||
continue
|
||||
}
|
||||
// Even if the alert provider returns an error, we still set the alert's Triggered variable to false.
|
||||
// Further explanation can be found on Alert's Triggered field.
|
||||
alert.Triggered = false
|
||||
if !alert.SendOnResolved {
|
||||
continue
|
||||
}
|
||||
alertProvider := config.GetAlertingProviderByAlertType(cfg, alert.Type)
|
||||
if alertProvider != nil && alertProvider.IsValid() {
|
||||
log.Printf("[watchdog][handleAlertsToResolve] Sending %s alert because alert with description='%s' has been RESOLVED", alert.Type, alert.Description)
|
||||
log.Printf("[watchdog][handleAlertsToResolve] Sending %s alert because alert for service=%s with description='%s' has been RESOLVED", alert.Type, service.Name, alert.Description)
|
||||
customAlertProvider := alertProvider.ToCustomAlertProvider(service, alert, result, true)
|
||||
// TODO: retry on error
|
||||
_, err := customAlertProvider.Send(service.Name, alert.Description, true)
|
||||
if err != nil {
|
||||
log.Printf("[watchdog][handleAlertsToResolve] Ran into error sending an alert: %s", err.Error())
|
||||
log.Printf("[watchdog][handleAlertsToResolve] Failed to send an alert for service=%s: %s", service.Name, err.Error())
|
||||
} else {
|
||||
if alert.Type == core.PagerDutyAlert {
|
||||
alert.ResolveKey = ""
|
||||
|
||||
332
watchdog/alerting_test.go
Normal file
332
watchdog/alerting_test.go
Normal file
@@ -0,0 +1,332 @@
|
||||
package watchdog
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/TwinProduction/gatus/alerting"
|
||||
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
||||
"github.com/TwinProduction/gatus/alerting/provider/pagerduty"
|
||||
"github.com/TwinProduction/gatus/config"
|
||||
"github.com/TwinProduction/gatus/core"
|
||||
)
|
||||
|
||||
func TestHandleAlerting(t *testing.T) {
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER", "true")
|
||||
defer os.Clearenv()
|
||||
|
||||
cfg := &config.Config{
|
||||
Debug: true,
|
||||
Alerting: &alerting.Config{
|
||||
Custom: &custom.AlertProvider{
|
||||
URL: "https://twinnation.org/health",
|
||||
Method: "GET",
|
||||
},
|
||||
},
|
||||
}
|
||||
config.Set(cfg)
|
||||
service := &core.Service{
|
||||
URL: "http://example.com",
|
||||
Alerts: []*core.Alert{
|
||||
{
|
||||
Type: core.CustomAlert,
|
||||
Enabled: true,
|
||||
FailureThreshold: 2,
|
||||
SuccessThreshold: 3,
|
||||
SendOnResolved: true,
|
||||
Triggered: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
verify(t, service, 0, 0, false, "The alert shouldn't start triggered")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
verify(t, service, 1, 0, false, "The alert shouldn't have triggered")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
verify(t, service, 2, 0, true, "The alert should've triggered")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
verify(t, service, 3, 0, true, "The alert should still be triggered")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
verify(t, service, 4, 0, true, "The alert should still be triggered")
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
verify(t, service, 0, 1, true, "The alert should still be triggered (because service.Alerts[0].SuccessThreshold is 3)")
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
verify(t, service, 0, 2, true, "The alert should still be triggered (because service.Alerts[0].SuccessThreshold is 3)")
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
verify(t, service, 0, 3, false, "The alert should've been resolved")
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
verify(t, service, 0, 4, false, "The alert should no longer be triggered")
|
||||
}
|
||||
|
||||
func TestHandleAlertingWhenAlertingConfigIsNil(t *testing.T) {
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER", "true")
|
||||
defer os.Clearenv()
|
||||
|
||||
cfg := &config.Config{
|
||||
Debug: true,
|
||||
Alerting: nil,
|
||||
}
|
||||
config.Set(cfg)
|
||||
HandleAlerting(nil, nil)
|
||||
}
|
||||
|
||||
func TestHandleAlertingWithBadAlertProvider(t *testing.T) {
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER", "true")
|
||||
defer os.Clearenv()
|
||||
|
||||
cfg := &config.Config{
|
||||
Alerting: &alerting.Config{},
|
||||
}
|
||||
config.Set(cfg)
|
||||
service := &core.Service{
|
||||
URL: "http://example.com",
|
||||
Alerts: []*core.Alert{
|
||||
{
|
||||
Type: core.CustomAlert,
|
||||
Enabled: true,
|
||||
FailureThreshold: 1,
|
||||
SuccessThreshold: 1,
|
||||
SendOnResolved: true,
|
||||
Triggered: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
verify(t, service, 0, 0, false, "The alert shouldn't start triggered")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
verify(t, service, 1, 0, false, "The alert shouldn't have triggered")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
verify(t, service, 2, 0, false, "The alert shouldn't have triggered, because the provider wasn't configured properly")
|
||||
}
|
||||
|
||||
func TestHandleAlertingWhenTriggeredAlertIsAlmostResolvedButServiceStartFailingAgain(t *testing.T) {
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER", "true")
|
||||
defer os.Clearenv()
|
||||
|
||||
cfg := &config.Config{
|
||||
Debug: true,
|
||||
Alerting: &alerting.Config{
|
||||
Custom: &custom.AlertProvider{
|
||||
URL: "https://twinnation.org/health",
|
||||
Method: "GET",
|
||||
},
|
||||
},
|
||||
}
|
||||
config.Set(cfg)
|
||||
service := &core.Service{
|
||||
URL: "http://example.com",
|
||||
Alerts: []*core.Alert{
|
||||
{
|
||||
Type: core.CustomAlert,
|
||||
Enabled: true,
|
||||
FailureThreshold: 2,
|
||||
SuccessThreshold: 3,
|
||||
SendOnResolved: true,
|
||||
Triggered: true,
|
||||
},
|
||||
},
|
||||
NumberOfFailuresInARow: 1,
|
||||
}
|
||||
|
||||
// This test simulate an alert that was already triggered
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
verify(t, service, 2, 0, true, "The alert was already triggered at the beginning of this test")
|
||||
}
|
||||
|
||||
func TestHandleAlertingWhenTriggeredAlertIsResolvedButSendOnResolvedIsFalse(t *testing.T) {
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER", "true")
|
||||
defer os.Clearenv()
|
||||
|
||||
cfg := &config.Config{
|
||||
Debug: true,
|
||||
Alerting: &alerting.Config{
|
||||
Custom: &custom.AlertProvider{
|
||||
URL: "https://twinnation.org/health",
|
||||
Method: "GET",
|
||||
},
|
||||
},
|
||||
}
|
||||
config.Set(cfg)
|
||||
service := &core.Service{
|
||||
URL: "http://example.com",
|
||||
Alerts: []*core.Alert{
|
||||
{
|
||||
Type: core.CustomAlert,
|
||||
Enabled: true,
|
||||
FailureThreshold: 1,
|
||||
SuccessThreshold: 1,
|
||||
SendOnResolved: false,
|
||||
Triggered: true,
|
||||
},
|
||||
},
|
||||
NumberOfFailuresInARow: 1,
|
||||
}
|
||||
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
verify(t, service, 0, 1, false, "The alert should've been resolved")
|
||||
}
|
||||
|
||||
func TestHandleAlertingWhenTriggeredAlertIsResolvedPagerDuty(t *testing.T) {
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER", "true")
|
||||
defer os.Clearenv()
|
||||
|
||||
cfg := &config.Config{
|
||||
Debug: true,
|
||||
Alerting: &alerting.Config{
|
||||
PagerDuty: &pagerduty.AlertProvider{
|
||||
IntegrationKey: "00000000000000000000000000000000",
|
||||
},
|
||||
},
|
||||
}
|
||||
config.Set(cfg)
|
||||
service := &core.Service{
|
||||
URL: "http://example.com",
|
||||
Alerts: []*core.Alert{
|
||||
{
|
||||
Type: core.PagerDutyAlert,
|
||||
Enabled: true,
|
||||
FailureThreshold: 1,
|
||||
SuccessThreshold: 1,
|
||||
SendOnResolved: true,
|
||||
Triggered: false,
|
||||
},
|
||||
},
|
||||
NumberOfFailuresInARow: 0,
|
||||
}
|
||||
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
verify(t, service, 1, 0, true, "")
|
||||
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
verify(t, service, 0, 1, false, "The alert should've been resolved")
|
||||
}
|
||||
|
||||
func TestHandleAlertingWithProviderThatReturnsAnError(t *testing.T) {
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER", "true")
|
||||
defer os.Clearenv()
|
||||
|
||||
cfg := &config.Config{
|
||||
Debug: true,
|
||||
Alerting: &alerting.Config{
|
||||
Custom: &custom.AlertProvider{
|
||||
URL: "https://twinnation.org/health",
|
||||
Method: "GET",
|
||||
},
|
||||
},
|
||||
}
|
||||
config.Set(cfg)
|
||||
service := &core.Service{
|
||||
URL: "http://example.com",
|
||||
Alerts: []*core.Alert{
|
||||
{
|
||||
Type: core.CustomAlert,
|
||||
Enabled: true,
|
||||
FailureThreshold: 2,
|
||||
SuccessThreshold: 2,
|
||||
SendOnResolved: true,
|
||||
Triggered: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "true")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
verify(t, service, 1, 0, false, "")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
verify(t, service, 2, 0, false, "The alert should have failed to trigger, because the alert provider is returning an error")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
verify(t, service, 3, 0, false, "The alert should still not be triggered, because the alert provider is still returning an error")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
verify(t, service, 4, 0, false, "The alert should still not be triggered, because the alert provider is still returning an error")
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "false")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
verify(t, service, 5, 0, true, "The alert should've been triggered because the alert provider is no longer returning an error")
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
verify(t, service, 0, 1, true, "The alert should've still been triggered")
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "true")
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
verify(t, service, 0, 2, false, "The alert should've been resolved DESPITE THE ALERT PROVIDER RETURNING AN ERROR. See Alert.Triggered for further explanation.")
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "false")
|
||||
|
||||
// Make sure that everything's working as expected after a rough patch
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
verify(t, service, 1, 0, false, "")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
verify(t, service, 2, 0, true, "The alert should have triggered")
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
verify(t, service, 0, 1, true, "The alert should still be triggered")
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
verify(t, service, 0, 2, false, "The alert should have been resolved")
|
||||
}
|
||||
|
||||
func TestHandleAlertingWithProviderThatOnlyReturnsErrorOnResolve(t *testing.T) {
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER", "true")
|
||||
defer os.Clearenv()
|
||||
|
||||
cfg := &config.Config{
|
||||
Debug: true,
|
||||
Alerting: &alerting.Config{
|
||||
Custom: &custom.AlertProvider{
|
||||
URL: "https://twinnation.org/health",
|
||||
Method: "GET",
|
||||
},
|
||||
},
|
||||
}
|
||||
config.Set(cfg)
|
||||
service := &core.Service{
|
||||
URL: "http://example.com",
|
||||
Alerts: []*core.Alert{
|
||||
{
|
||||
Type: core.CustomAlert,
|
||||
Enabled: true,
|
||||
FailureThreshold: 1,
|
||||
SuccessThreshold: 1,
|
||||
SendOnResolved: true,
|
||||
Triggered: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
verify(t, service, 1, 0, true, "")
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "true")
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
verify(t, service, 0, 1, false, "")
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "false")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
verify(t, service, 1, 0, true, "")
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "true")
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
verify(t, service, 0, 1, false, "")
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "false")
|
||||
|
||||
// Make sure that everything's working as expected after a rough patch
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
verify(t, service, 1, 0, true, "")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
verify(t, service, 2, 0, true, "")
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
verify(t, service, 0, 1, false, "")
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
verify(t, service, 0, 2, false, "")
|
||||
}
|
||||
|
||||
func verify(t *testing.T, service *core.Service, expectedNumberOfFailuresInARow, expectedNumberOfSuccessInARow int, expectedTriggered bool, expectedTriggeredReason string) {
|
||||
if service.NumberOfFailuresInARow != expectedNumberOfFailuresInARow {
|
||||
t.Fatalf("service.NumberOfFailuresInARow should've been %d, got %d", expectedNumberOfFailuresInARow, service.NumberOfFailuresInARow)
|
||||
}
|
||||
if service.NumberOfSuccessesInARow != expectedNumberOfSuccessInARow {
|
||||
t.Fatalf("service.NumberOfSuccessesInARow should've been %d, got %d", expectedNumberOfSuccessInARow, service.NumberOfSuccessesInARow)
|
||||
}
|
||||
if service.Alerts[0].Triggered != expectedTriggered {
|
||||
if len(expectedTriggeredReason) != 0 {
|
||||
t.Fatal(expectedTriggeredReason)
|
||||
} else {
|
||||
if expectedTriggered {
|
||||
t.Fatal("The alert should've been triggered")
|
||||
} else {
|
||||
t.Fatal("The alert shouldn't have been triggered")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package watchdog
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
@@ -21,10 +20,9 @@ var (
|
||||
monitoringMutex sync.Mutex
|
||||
)
|
||||
|
||||
// GetServiceStatusesAsJSON returns a list of core.ServiceStatus for each services encoded using json.Marshal.
|
||||
// GetServiceStatusesAsJSON the JSON encoding of all core.ServiceStatus recorded
|
||||
func GetServiceStatusesAsJSON() ([]byte, error) {
|
||||
serviceStatuses := store.GetAll()
|
||||
return json.Marshal(serviceStatuses)
|
||||
return store.GetAllAsJSON()
|
||||
}
|
||||
|
||||
// GetUptimeByServiceGroupAndName returns the uptime of a service based on its group and name
|
||||
|
||||
Reference in New Issue
Block a user