Compare commits

..

8 Commits

Author SHA1 Message Date
TwinProduction
1b995e0d48 Fix typo 2020-10-17 16:20:31 -04:00
TwinProduction
030316b8d4 Update documentation 2020-10-17 16:12:40 -04:00
TwinProduction
0a7988f2ff Add disable-monitoring-lock configuration parameter 2020-10-16 23:07:14 -04:00
TwinProduction
816bc95905 Return yaml unmarshal error 2020-10-16 12:12:00 -04:00
TwinProduction
58b9b17944 Trim down size of condition to display on invalid path 2020-10-15 22:44:34 -04:00
TwinProduction
a66cfc094a Ignore "unexpected end of JSON input" errors 2020-10-15 22:43:24 -04:00
TwinProduction
8fd6eddc16 Make sure that the SHA512 hash is lowercase 2020-10-14 21:45:45 -04:00
TwinProduction
402525d572 Update Kubernetes example 2020-10-14 20:07:56 -04:00
7 changed files with 85 additions and 11 deletions

View File

@@ -34,6 +34,7 @@ core applications: https://status.twinnation.org/
- [Recommended interval](#recommended-interval)
- [Default timeouts](#default-timeouts)
- [Monitoring a TCP service](#monitoring-a-tcp-service)
- [disable-monitoring-lock](#disable-monitoring-lock)
## Features
@@ -119,6 +120,7 @@ Note that you can also add environment variables in the configuration file (i.e.
| `security.basic` | Basic authentication security configuration | `{}` |
| `security.basic.username` | Username for Basic authentication | Required `""` |
| `security.basic.password-sha512` | Password's SHA512 hash for Basic authentication | Required `""` |
| `disable-monitoring-lock` | Whether to [disable the monitoring lock](#disable-monitoring-lock) | `false` |
### Conditions
@@ -361,7 +363,10 @@ will send a `POST` request to `http://localhost:8080/playground` with the follow
### Recommended interval
To ensure that Gatus provides reliable and accurate results (i.e. response time), Gatus only evaluates one service at a time.
**NOTE**: This does not _really_ apply if `disable-monitoring-lock` is set to `true`, as the monitoring lock is what
tells Gatus to only evaluate one service at a time.
To ensure that Gatus provides reliable and accurate results (i.e. response time), Gatus only evaluates one service at a time
In other words, even if you have multiple services with the exact same interval, they will not execute at the same time.
You can test this yourself by running Gatus with several services configured with a very short, unrealistic interval,
@@ -428,3 +433,17 @@ security:
```
The example above will require that you authenticate with the username `john.doe` as well as the password `hunter2`.
### disable-monitoring-lock
Setting `disable-monitoring-lock` to `true` means that multiple services could be monitored at the same time.
While this behavior wouldn't generally be harmful, conditions using the `[RESPONSE_TIME]` placeholder could be impacted
by the evaluation of multiple services at the same time, therefore, the default value for this parameter is `false`.
There are three main reasons why you might want to disable the monitoring lock:
- You're using Gatus for load testing (each services are periodically evaluated on a different goroutine, so
technically, if you create 100 services with a 1 seconds interval, Gatus will send 100 requests per second)
- You have a _lot_ of services to monitor
- You want to test multiple services at very short interval (< 5s)

View File

@@ -28,11 +28,25 @@ var (
// Config is the main configuration structure
type Config struct {
Metrics bool `yaml:"metrics"`
Debug bool `yaml:"debug"`
// Debug Whether to enable debug logs
Debug bool `yaml:"debug"`
// Metrics Whether to expose metrics at /metrics
Metrics bool `yaml:"metrics"`
// DisableMonitoringLock Whether to disable the monitoring lock
// The monitoring lock is what prevents multiple services from being processed at the same time.
// Disabling this may lead to inaccurate response times
DisableMonitoringLock bool `yaml:"disable-monitoring-lock"`
// Security Configuration for securing access to Gatus
Security *security.Config `yaml:"security"`
// Alerting Configuration for alerting
Alerting *alerting.Config `yaml:"alerting"`
Services []*core.Service `yaml:"services"`
// Services List of services to monitor
Services []*core.Service `yaml:"services"`
}
func Get() *Config {
@@ -81,6 +95,9 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) {
yamlBytes = []byte(os.ExpandEnv(string(yamlBytes)))
// Parse configuration file
err = yaml.Unmarshal(yamlBytes, &config)
if err != nil {
return
}
// Check if the configuration file at least has services.
if config == nil || config.Services == nil || len(config.Services) == 0 {
err = ErrNoServiceInConfig

View File

@@ -82,7 +82,15 @@ func (c *Condition) evaluate(result *Result) bool {
// 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)
conditionToDisplay = fmt.Sprintf("%s (%s)", 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)
}
}
result.ConditionResults = append(result.ConditionResults, &ConditionResult{Condition: conditionToDisplay, Success: success})
return success
@@ -138,7 +146,9 @@ func sanitizeAndResolve(list []string, result *Result) []string {
}
resolvedElement, resolvedElementLength, err := jsonpath.Eval(strings.Replace(element, fmt.Sprintf("%s.", BodyPlaceHolder), "", 1), result.Body)
if err != nil {
result.Errors = append(result.Errors, err.Error())
if err.Error() != "unexpected end of JSON input" {
result.Errors = append(result.Errors, err.Error())
}
element = fmt.Sprintf("%s %s", element, InvalidConditionElementSuffix)
} else {
if wantLength {

View File

@@ -113,6 +113,19 @@ func TestCondition_evaluateWithBodyJsonPathComplex(t *testing.T) {
}
}
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)
}
}
func TestCondition_evaluateWithBodyJsonPathDoublePlaceholders(t *testing.T) {
condition := Condition("[BODY].user.firstName != [BODY].user.lastName")
result := &Result{Body: []byte("{\"user\": {\"firstName\": \"john\", \"lastName\": \"doe\"}}")}

View File

@@ -13,6 +13,16 @@ data:
interval: 5m
conditions:
- "[STATUS] == 200"
- name: cat-fact
url: "https://cat-fact.herokuapp.com/facts/random"
interval: 5m
conditions:
- "[STATUS] == 200"
- "[BODY].deleted == false"
- "len([BODY].text) > 0"
- "[BODY].text == pat(*cat*)"
- "[STATUS] == pat(2*)"
- "[CONNECTED] == true"
- name: Example
url: https://example.com/
conditions:

View File

@@ -2,12 +2,13 @@ package security
import (
"net/http"
"strings"
)
func Handler(handler http.HandlerFunc, security *Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
usernameEntered, passwordEntered, ok := r.BasicAuth()
if !ok || usernameEntered != security.Basic.Username || Sha512(passwordEntered) != security.Basic.PasswordSha512Hash {
if !ok || usernameEntered != security.Basic.Username || Sha512(passwordEntered) != strings.ToLower(security.Basic.PasswordSha512Hash) {
w.Header().Set("WWW-Authenticate", "Basic")
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte("Unauthorized"))

View File

@@ -44,9 +44,11 @@ func Monitor(cfg *config.Config) {
func monitor(service *core.Service) {
cfg := config.Get()
for {
// By placing the lock here, we prevent multiple services from being monitored at the exact same time, which
// could cause performance issues and return inaccurate results
monitoringMutex.Lock()
if !cfg.DisableMonitoringLock {
// By placing the lock here, we prevent multiple services from being monitored at the exact same time, which
// could cause performance issues and return inaccurate results
monitoringMutex.Lock()
}
if cfg.Debug {
log.Printf("[watchdog][monitor] Monitoring serviceName=%s", service.Name)
}
@@ -74,7 +76,9 @@ func monitor(service *core.Service) {
if cfg.Debug {
log.Printf("[watchdog][monitor] Waiting for interval=%s before monitoring serviceName=%s again", service.Interval, service.Name)
}
monitoringMutex.Unlock()
if !cfg.DisableMonitoringLock {
monitoringMutex.Unlock()
}
time.Sleep(service.Interval)
}
}