mirror of
https://github.com/TwiN/gatus.git
synced 2024-12-14 11:58:04 +00:00
Add configuration for whether to resolve failed conditions or not
This commit is contained in:
parent
d7de795a9f
commit
f41560cd3e
6 changed files with 53 additions and 31 deletions
11
README.md
11
README.md
|
@ -142,7 +142,7 @@ If you want to test it locally, see [Docker](#docker).
|
||||||
|:---------------------------------------- |:----------------------------------------------------------------------------- |:-------------- |
|
|:---------------------------------------- |:----------------------------------------------------------------------------- |:-------------- |
|
||||||
| `debug` | Whether to enable debug logs. | `false` |
|
| `debug` | Whether to enable debug logs. | `false` |
|
||||||
| `metrics` | Whether to expose metrics at /metrics. | `false` |
|
| `metrics` | Whether to expose metrics at /metrics. | `false` |
|
||||||
| `storage` | Storage configuration. <br />See [Storage](#storage). | `{}` |
|
| `storage` | [Storage configuration](#storage) | `{}` |
|
||||||
| `services` | List of services to monitor. | Required `[]` |
|
| `services` | List of services to monitor. | Required `[]` |
|
||||||
| `services[].name` | Name of the service. Can be anything. | Required `""` |
|
| `services[].name` | Name of the service. Can be anything. | Required `""` |
|
||||||
| `services[].group` | Group name. Used to group multiple services together on the dashboard. <br />See [Service groups](#service-groups). | `""` |
|
| `services[].group` | Group name. Used to group multiple services together on the dashboard. <br />See [Service groups](#service-groups). | `""` |
|
||||||
|
@ -156,16 +156,17 @@ If you want to test it locally, see [Docker](#docker).
|
||||||
| `services[].dns` | Configuration for a service of type DNS. <br />See [Monitoring a service using DNS queries](#monitoring-a-service-using-dns-queries). | `""` |
|
| `services[].dns` | Configuration for a service of type DNS. <br />See [Monitoring a service using DNS queries](#monitoring-a-service-using-dns-queries). | `""` |
|
||||||
| `services[].dns.query-type` | Query type for DNS service. | `""` |
|
| `services[].dns.query-type` | Query type for DNS service. | `""` |
|
||||||
| `services[].dns.query-name` | Query name for DNS service. | `""` |
|
| `services[].dns.query-name` | Query name for DNS service. | `""` |
|
||||||
| `services[].alerts[].type` | Type of alert. Valid types: `slack`, `discord`, `pagerduty`, `twilio`, `mattermost`, `messagebird`, `teams` `custom`. | Required `""` |
|
| `services[].alerts[].type` | Type of alert. <br />Valid types: `slack`, `discord`, `pagerduty`, `twilio`, `mattermost`, `messagebird`, `teams` `custom`. | Required `""` |
|
||||||
| `services[].alerts[].enabled` | Whether to enable the alert. | `false` |
|
| `services[].alerts[].enabled` | Whether to enable the alert. | `false` |
|
||||||
| `services[].alerts[].failure-threshold` | Number of failures in a row needed before triggering the alert. | `3` |
|
| `services[].alerts[].failure-threshold` | Number of failures in a row needed before triggering the alert. | `3` |
|
||||||
| `services[].alerts[].success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved. | `2` |
|
| `services[].alerts[].success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved. | `2` |
|
||||||
| `services[].alerts[].send-on-resolved` | Whether to send a notification once a triggered alert is marked as resolved. | `false` |
|
| `services[].alerts[].send-on-resolved` | Whether to send a notification once a triggered alert is marked as resolved. | `false` |
|
||||||
| `services[].alerts[].description` | Description of the alert. Will be included in the alert sent. | `""` |
|
| `services[].alerts[].description` | Description of the alert. Will be included in the alert sent. | `""` |
|
||||||
| `services[].client` | Client configuration. <br />See [Client configuration](#client-configuration). | `{}` |
|
| `services[].client` | [Client configuration](#client-configuration). | `{}` |
|
||||||
| `services[].ui` | UI configuration. | `{}` |
|
| `services[].ui` | UI configuration at the service level. | `{}` |
|
||||||
| `services[].ui.hide-hostname` | Whether to include the hostname in the result. | `false` |
|
| `services[].ui.hide-hostname` | Whether to include the hostname in the result. | `false` |
|
||||||
| `alerting` | Configuration for alerting. <br />See [Alerting](#alerting). | `{}` |
|
| `services[].ui.dont-resolve-failed-conditions` | Whether to resolve failed conditions for the UI. | `false` |
|
||||||
|
| `alerting` | [Alerting configuration](#alerting). | `{}` |
|
||||||
| `security` | Security configuration. | `{}` |
|
| `security` | Security configuration. | `{}` |
|
||||||
| `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 `""` |
|
||||||
|
|
|
@ -85,44 +85,44 @@ type Condition string
|
||||||
|
|
||||||
// evaluate the Condition with the Result of the health check
|
// evaluate the Condition with the Result of the health check
|
||||||
// TODO: Add a mandatory space between each operators (e.g. " == " instead of "==") (BREAKING CHANGE)
|
// TODO: Add a mandatory space between each operators (e.g. " == " instead of "==") (BREAKING CHANGE)
|
||||||
func (c Condition) evaluate(result *Result) bool {
|
func (c Condition) evaluate(result *Result, dontResolveFailedConditions bool) bool {
|
||||||
condition := string(c)
|
condition := string(c)
|
||||||
success := false
|
success := false
|
||||||
conditionToDisplay := condition
|
conditionToDisplay := condition
|
||||||
if strings.Contains(condition, "==") {
|
if strings.Contains(condition, "==") {
|
||||||
parameters, resolvedParameters := sanitizeAndResolve(strings.Split(condition, "=="), result)
|
parameters, resolvedParameters := sanitizeAndResolve(strings.Split(condition, "=="), result)
|
||||||
success = isEqual(resolvedParameters[0], resolvedParameters[1])
|
success = isEqual(resolvedParameters[0], resolvedParameters[1])
|
||||||
if !success {
|
if !success && !dontResolveFailedConditions {
|
||||||
conditionToDisplay = prettify(parameters, resolvedParameters, "==")
|
conditionToDisplay = prettify(parameters, resolvedParameters, "==")
|
||||||
}
|
}
|
||||||
} else if strings.Contains(condition, "!=") {
|
} else if strings.Contains(condition, "!=") {
|
||||||
parameters, resolvedParameters := sanitizeAndResolve(strings.Split(condition, "!="), result)
|
parameters, resolvedParameters := sanitizeAndResolve(strings.Split(condition, "!="), result)
|
||||||
success = !isEqual(resolvedParameters[0], resolvedParameters[1])
|
success = !isEqual(resolvedParameters[0], resolvedParameters[1])
|
||||||
if !success {
|
if !success && !dontResolveFailedConditions {
|
||||||
conditionToDisplay = prettify(parameters, resolvedParameters, "!=")
|
conditionToDisplay = prettify(parameters, resolvedParameters, "!=")
|
||||||
}
|
}
|
||||||
} else if strings.Contains(condition, "<=") {
|
} else if strings.Contains(condition, "<=") {
|
||||||
parameters, resolvedParameters := sanitizeAndResolveNumerical(strings.Split(condition, "<="), result)
|
parameters, resolvedParameters := sanitizeAndResolveNumerical(strings.Split(condition, "<="), result)
|
||||||
success = resolvedParameters[0] <= resolvedParameters[1]
|
success = resolvedParameters[0] <= resolvedParameters[1]
|
||||||
if !success {
|
if !success && !dontResolveFailedConditions {
|
||||||
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, "<=")
|
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, "<=")
|
||||||
}
|
}
|
||||||
} else if strings.Contains(condition, ">=") {
|
} else if strings.Contains(condition, ">=") {
|
||||||
parameters, resolvedParameters := sanitizeAndResolveNumerical(strings.Split(condition, ">="), result)
|
parameters, resolvedParameters := sanitizeAndResolveNumerical(strings.Split(condition, ">="), result)
|
||||||
success = resolvedParameters[0] >= resolvedParameters[1]
|
success = resolvedParameters[0] >= resolvedParameters[1]
|
||||||
if !success {
|
if !success && !dontResolveFailedConditions {
|
||||||
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, ">=")
|
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, ">=")
|
||||||
}
|
}
|
||||||
} else if strings.Contains(condition, ">") {
|
} else if strings.Contains(condition, ">") {
|
||||||
parameters, resolvedParameters := sanitizeAndResolveNumerical(strings.Split(condition, ">"), result)
|
parameters, resolvedParameters := sanitizeAndResolveNumerical(strings.Split(condition, ">"), result)
|
||||||
success = resolvedParameters[0] > resolvedParameters[1]
|
success = resolvedParameters[0] > resolvedParameters[1]
|
||||||
if !success {
|
if !success && !dontResolveFailedConditions {
|
||||||
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, ">")
|
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, ">")
|
||||||
}
|
}
|
||||||
} else if strings.Contains(condition, "<") {
|
} else if strings.Contains(condition, "<") {
|
||||||
parameters, resolvedParameters := sanitizeAndResolveNumerical(strings.Split(condition, "<"), result)
|
parameters, resolvedParameters := sanitizeAndResolveNumerical(strings.Split(condition, "<"), result)
|
||||||
success = resolvedParameters[0] < resolvedParameters[1]
|
success = resolvedParameters[0] < resolvedParameters[1]
|
||||||
if !success {
|
if !success && !dontResolveFailedConditions {
|
||||||
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, "<")
|
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, "<")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -6,7 +6,7 @@ func BenchmarkCondition_evaluateWithBodyStringAny(b *testing.B) {
|
||||||
condition := Condition("[BODY].name == any(john.doe, jane.doe)")
|
condition := Condition("[BODY].name == any(john.doe, jane.doe)")
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
result := &Result{body: []byte("{\"name\": \"john.doe\"}")}
|
result := &Result{body: []byte("{\"name\": \"john.doe\"}")}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result, false)
|
||||||
}
|
}
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ func BenchmarkCondition_evaluateWithBodyStringAnyFailure(b *testing.B) {
|
||||||
condition := Condition("[BODY].name == any(john.doe, jane.doe)")
|
condition := Condition("[BODY].name == any(john.doe, jane.doe)")
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
result := &Result{body: []byte("{\"name\": \"bob.doe\"}")}
|
result := &Result{body: []byte("{\"name\": \"bob.doe\"}")}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result, false)
|
||||||
}
|
}
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ func BenchmarkCondition_evaluateWithBodyString(b *testing.B) {
|
||||||
condition := Condition("[BODY].name == john.doe")
|
condition := Condition("[BODY].name == john.doe")
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
result := &Result{body: []byte("{\"name\": \"john.doe\"}")}
|
result := &Result{body: []byte("{\"name\": \"john.doe\"}")}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result, false)
|
||||||
}
|
}
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ func BenchmarkCondition_evaluateWithBodyStringFailure(b *testing.B) {
|
||||||
condition := Condition("[BODY].name == john.doe")
|
condition := Condition("[BODY].name == john.doe")
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
result := &Result{body: []byte("{\"name\": \"bob.doe\"}")}
|
result := &Result{body: []byte("{\"name\": \"bob.doe\"}")}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result, false)
|
||||||
}
|
}
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ func BenchmarkCondition_evaluateWithBodyStringFailureInvalidPath(b *testing.B) {
|
||||||
condition := Condition("[BODY].user.name == bob.doe")
|
condition := Condition("[BODY].user.name == bob.doe")
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
result := &Result{body: []byte("{\"name\": \"bob.doe\"}")}
|
result := &Result{body: []byte("{\"name\": \"bob.doe\"}")}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result, false)
|
||||||
}
|
}
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ func BenchmarkCondition_evaluateWithBodyStringLen(b *testing.B) {
|
||||||
condition := Condition("len([BODY].name) == 8")
|
condition := Condition("len([BODY].name) == 8")
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
result := &Result{body: []byte("{\"name\": \"john.doe\"}")}
|
result := &Result{body: []byte("{\"name\": \"john.doe\"}")}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result, false)
|
||||||
}
|
}
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ func BenchmarkCondition_evaluateWithBodyStringLenFailure(b *testing.B) {
|
||||||
condition := Condition("len([BODY].name) == 8")
|
condition := Condition("len([BODY].name) == 8")
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
result := &Result{body: []byte("{\"name\": \"bob.doe\"}")}
|
result := &Result{body: []byte("{\"name\": \"bob.doe\"}")}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result, false)
|
||||||
}
|
}
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,7 @@ func BenchmarkCondition_evaluateWithStatus(b *testing.B) {
|
||||||
condition := Condition("[STATUS] == 200")
|
condition := Condition("[STATUS] == 200")
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
result := &Result{HTTPStatus: 200}
|
result := &Result{HTTPStatus: 200}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result, false)
|
||||||
}
|
}
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
}
|
}
|
||||||
|
@ -78,7 +78,7 @@ func BenchmarkCondition_evaluateWithStatusFailure(b *testing.B) {
|
||||||
condition := Condition("[STATUS] == 200")
|
condition := Condition("[STATUS] == 200")
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
result := &Result{HTTPStatus: 400}
|
result := &Result{HTTPStatus: 400}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result, false)
|
||||||
}
|
}
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ func TestCondition_evaluate(t *testing.T) {
|
||||||
Name string
|
Name string
|
||||||
Condition Condition
|
Condition Condition
|
||||||
Result *Result
|
Result *Result
|
||||||
|
DontResolveFailedConditions bool
|
||||||
ExpectedSuccess bool
|
ExpectedSuccess bool
|
||||||
ExpectedOutput string
|
ExpectedOutput string
|
||||||
}
|
}
|
||||||
|
@ -372,6 +373,14 @@ func TestCondition_evaluate(t *testing.T) {
|
||||||
ExpectedSuccess: false,
|
ExpectedSuccess: false,
|
||||||
ExpectedOutput: "[STATUS] (404) == any(200, 429)",
|
ExpectedOutput: "[STATUS] (404) == any(200, 429)",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "status-any-failure-but-dont-resolve",
|
||||||
|
Condition: Condition("[STATUS] == any(200, 429)"),
|
||||||
|
Result: &Result{HTTPStatus: 404},
|
||||||
|
DontResolveFailedConditions: true,
|
||||||
|
ExpectedSuccess: false,
|
||||||
|
ExpectedOutput: "[STATUS] == any(200, 429)",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "connected",
|
Name: "connected",
|
||||||
Condition: Condition("[CONNECTED] == true"),
|
Condition: Condition("[CONNECTED] == true"),
|
||||||
|
@ -435,6 +444,14 @@ func TestCondition_evaluate(t *testing.T) {
|
||||||
ExpectedSuccess: false,
|
ExpectedSuccess: false,
|
||||||
ExpectedOutput: "has([BODY].errors) (true) == false",
|
ExpectedOutput: "has([BODY].errors) (true) == false",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "has-failure-but-dont-resolve",
|
||||||
|
Condition: Condition("has([BODY].errors) == false"),
|
||||||
|
Result: &Result{body: []byte("{\"errors\": [\"1\"]}")},
|
||||||
|
DontResolveFailedConditions: true,
|
||||||
|
ExpectedSuccess: false,
|
||||||
|
ExpectedOutput: "has([BODY].errors) == false",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "no-placeholders",
|
Name: "no-placeholders",
|
||||||
Condition: Condition("1 == 2"),
|
Condition: Condition("1 == 2"),
|
||||||
|
@ -445,7 +462,7 @@ func TestCondition_evaluate(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
scenario.Condition.evaluate(scenario.Result)
|
scenario.Condition.evaluate(scenario.Result, scenario.DontResolveFailedConditions)
|
||||||
if scenario.Result.ConditionResults[0].Success != scenario.ExpectedSuccess {
|
if scenario.Result.ConditionResults[0].Success != scenario.ExpectedSuccess {
|
||||||
t.Errorf("Condition '%s' should have been success=%v", scenario.Condition, scenario.ExpectedSuccess)
|
t.Errorf("Condition '%s' should have been success=%v", scenario.Condition, scenario.ExpectedSuccess)
|
||||||
}
|
}
|
||||||
|
@ -459,7 +476,7 @@ func TestCondition_evaluate(t *testing.T) {
|
||||||
func TestCondition_evaluateWithInvalidOperator(t *testing.T) {
|
func TestCondition_evaluateWithInvalidOperator(t *testing.T) {
|
||||||
condition := Condition("[STATUS] ? 201")
|
condition := Condition("[STATUS] ? 201")
|
||||||
result := &Result{HTTPStatus: 201}
|
result := &Result{HTTPStatus: 201}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result, false)
|
||||||
if result.Success {
|
if result.Success {
|
||||||
t.Error("condition was invalid, result should've been a failure")
|
t.Error("condition was invalid, result should've been a failure")
|
||||||
}
|
}
|
||||||
|
|
|
@ -163,7 +163,7 @@ func (service *Service) EvaluateHealth() *Result {
|
||||||
result.Success = false
|
result.Success = false
|
||||||
}
|
}
|
||||||
for _, condition := range service.Conditions {
|
for _, condition := range service.Conditions {
|
||||||
success := condition.evaluate(result)
|
success := condition.evaluate(result, service.UIConfig.DontResolveFailedConditions)
|
||||||
if !success {
|
if !success {
|
||||||
result.Success = false
|
result.Success = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,16 @@ package ui
|
||||||
|
|
||||||
// Config is the UI configuration for services
|
// Config is the UI configuration for services
|
||||||
type Config struct {
|
type Config struct {
|
||||||
HideHostname bool `yaml:"hide-hostname"` // Whether to hide the hostname in the Result
|
// HideHostname whether to hide the hostname in the Result
|
||||||
|
HideHostname bool `yaml:"hide-hostname"`
|
||||||
|
// DontResolveFailedConditions whether to resolve failed conditions in the Result for display in the UI
|
||||||
|
DontResolveFailedConditions bool `yaml:"dont-resolve-failed-conditions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultConfig retrieves the default UI configuration
|
// GetDefaultConfig retrieves the default UI configuration
|
||||||
func GetDefaultConfig() *Config {
|
func GetDefaultConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
HideHostname: false,
|
HideHostname: false,
|
||||||
|
DontResolveFailedConditions: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue