Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b995e0d48 | ||
|
|
030316b8d4 | ||
|
|
0a7988f2ff | ||
|
|
816bc95905 | ||
|
|
58b9b17944 | ||
|
|
a66cfc094a |
21
README.md
21
README.md
@@ -34,6 +34,7 @@ core applications: https://status.twinnation.org/
|
|||||||
- [Recommended interval](#recommended-interval)
|
- [Recommended interval](#recommended-interval)
|
||||||
- [Default timeouts](#default-timeouts)
|
- [Default timeouts](#default-timeouts)
|
||||||
- [Monitoring a TCP service](#monitoring-a-tcp-service)
|
- [Monitoring a TCP service](#monitoring-a-tcp-service)
|
||||||
|
- [disable-monitoring-lock](#disable-monitoring-lock)
|
||||||
|
|
||||||
|
|
||||||
## Features
|
## 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` | Basic authentication security configuration | `{}` |
|
||||||
| `security.basic.username` | Username for Basic authentication | Required `""` |
|
| `security.basic.username` | Username for Basic authentication | Required `""` |
|
||||||
| `security.basic.password-sha512` | Password's SHA512 hash 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
|
### Conditions
|
||||||
@@ -361,7 +363,10 @@ will send a `POST` request to `http://localhost:8080/playground` with the follow
|
|||||||
|
|
||||||
### Recommended interval
|
### 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.
|
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,
|
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`.
|
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)
|
||||||
|
|||||||
@@ -28,11 +28,25 @@ var (
|
|||||||
|
|
||||||
// Config is the main configuration structure
|
// Config is the main configuration structure
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Metrics bool `yaml:"metrics"`
|
// Debug Whether to enable debug logs
|
||||||
Debug bool `yaml:"debug"`
|
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"`
|
Security *security.Config `yaml:"security"`
|
||||||
|
|
||||||
|
// Alerting Configuration for alerting
|
||||||
Alerting *alerting.Config `yaml:"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 {
|
func Get() *Config {
|
||||||
@@ -81,6 +95,9 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) {
|
|||||||
yamlBytes = []byte(os.ExpandEnv(string(yamlBytes)))
|
yamlBytes = []byte(os.ExpandEnv(string(yamlBytes)))
|
||||||
// Parse configuration file
|
// Parse configuration file
|
||||||
err = yaml.Unmarshal(yamlBytes, &config)
|
err = yaml.Unmarshal(yamlBytes, &config)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
// Check if the configuration file at least has services.
|
// Check if the configuration file at least has services.
|
||||||
if config == nil || config.Services == nil || len(config.Services) == 0 {
|
if config == nil || config.Services == nil || len(config.Services) == 0 {
|
||||||
err = ErrNoServiceInConfig
|
err = ErrNoServiceInConfig
|
||||||
|
|||||||
@@ -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 the condition isn't a success, return what the resolved condition was too
|
||||||
if !success {
|
if !success {
|
||||||
log.Printf("[Condition][evaluate] Condition '%s' did not succeed because '%s' is false", condition, resolvedCondition)
|
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})
|
result.ConditionResults = append(result.ConditionResults, &ConditionResult{Condition: conditionToDisplay, Success: success})
|
||||||
return 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)
|
resolvedElement, resolvedElementLength, err := jsonpath.Eval(strings.Replace(element, fmt.Sprintf("%s.", BodyPlaceHolder), "", 1), result.Body)
|
||||||
if err != nil {
|
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)
|
element = fmt.Sprintf("%s %s", element, InvalidConditionElementSuffix)
|
||||||
} else {
|
} else {
|
||||||
if wantLength {
|
if wantLength {
|
||||||
|
|||||||
@@ -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) {
|
func TestCondition_evaluateWithBodyJsonPathDoublePlaceholders(t *testing.T) {
|
||||||
condition := Condition("[BODY].user.firstName != [BODY].user.lastName")
|
condition := Condition("[BODY].user.firstName != [BODY].user.lastName")
|
||||||
result := &Result{Body: []byte("{\"user\": {\"firstName\": \"john\", \"lastName\": \"doe\"}}")}
|
result := &Result{Body: []byte("{\"user\": {\"firstName\": \"john\", \"lastName\": \"doe\"}}")}
|
||||||
|
|||||||
@@ -44,9 +44,11 @@ func Monitor(cfg *config.Config) {
|
|||||||
func monitor(service *core.Service) {
|
func monitor(service *core.Service) {
|
||||||
cfg := config.Get()
|
cfg := config.Get()
|
||||||
for {
|
for {
|
||||||
// By placing the lock here, we prevent multiple services from being monitored at the exact same time, which
|
if !cfg.DisableMonitoringLock {
|
||||||
// could cause performance issues and return inaccurate results
|
// By placing the lock here, we prevent multiple services from being monitored at the exact same time, which
|
||||||
monitoringMutex.Lock()
|
// could cause performance issues and return inaccurate results
|
||||||
|
monitoringMutex.Lock()
|
||||||
|
}
|
||||||
if cfg.Debug {
|
if cfg.Debug {
|
||||||
log.Printf("[watchdog][monitor] Monitoring serviceName=%s", service.Name)
|
log.Printf("[watchdog][monitor] Monitoring serviceName=%s", service.Name)
|
||||||
}
|
}
|
||||||
@@ -74,7 +76,9 @@ func monitor(service *core.Service) {
|
|||||||
if cfg.Debug {
|
if cfg.Debug {
|
||||||
log.Printf("[watchdog][monitor] Waiting for interval=%s before monitoring serviceName=%s again", service.Interval, service.Name)
|
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)
|
time.Sleep(service.Interval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user