diff --git a/README.md b/README.md
index 83b1edbe..b5f2e714 100644
--- a/README.md
+++ b/README.md
@@ -142,7 +142,7 @@ If you want to test it locally, see [Docker](#docker).
|:---------------------------------------- |:----------------------------------------------------------------------------- |:-------------- |
| `debug` | Whether to enable debug logs. | `false` |
| `metrics` | Whether to expose metrics at /metrics. | `false` |
-| `storage` | Storage configuration.
See [Storage](#storage). | `{}` |
+| `storage` | [Storage configuration](#storage) | `{}` |
| `services` | List of services to monitor. | Required `[]` |
| `services[].name` | Name of the service. Can be anything. | Required `""` |
| `services[].group` | Group name. Used to group multiple services together on the dashboard.
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.
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-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.
Valid types: `slack`, `discord`, `pagerduty`, `twilio`, `mattermost`, `messagebird`, `teams` `custom`. | Required `""` |
| `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[].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[].description` | Description of the alert. Will be included in the alert sent. | `""` |
-| `services[].client` | Client configuration.
See [Client configuration](#client-configuration). | `{}` |
-| `services[].ui` | UI configuration. | `{}` |
+| `services[].client` | [Client configuration](#client-configuration). | `{}` |
+| `services[].ui` | UI configuration at the service level. | `{}` |
| `services[].ui.hide-hostname` | Whether to include the hostname in the result. | `false` |
-| `alerting` | Configuration for alerting.
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.basic` | Basic authentication security configuration. | `{}` |
| `security.basic.username` | Username for Basic authentication. | Required `""` |
diff --git a/core/condition.go b/core/condition.go
index 4bc33af3..3ee096b4 100644
--- a/core/condition.go
+++ b/core/condition.go
@@ -85,44 +85,44 @@ type Condition string
// evaluate the Condition with the Result of the health check
// 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)
success := false
conditionToDisplay := condition
if strings.Contains(condition, "==") {
parameters, resolvedParameters := sanitizeAndResolve(strings.Split(condition, "=="), result)
success = isEqual(resolvedParameters[0], resolvedParameters[1])
- if !success {
+ if !success && !dontResolveFailedConditions {
conditionToDisplay = prettify(parameters, resolvedParameters, "==")
}
} else if strings.Contains(condition, "!=") {
parameters, resolvedParameters := sanitizeAndResolve(strings.Split(condition, "!="), result)
success = !isEqual(resolvedParameters[0], resolvedParameters[1])
- if !success {
+ if !success && !dontResolveFailedConditions {
conditionToDisplay = prettify(parameters, resolvedParameters, "!=")
}
} else if strings.Contains(condition, "<=") {
parameters, resolvedParameters := sanitizeAndResolveNumerical(strings.Split(condition, "<="), result)
success = resolvedParameters[0] <= resolvedParameters[1]
- if !success {
+ if !success && !dontResolveFailedConditions {
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, "<=")
}
} else if strings.Contains(condition, ">=") {
parameters, resolvedParameters := sanitizeAndResolveNumerical(strings.Split(condition, ">="), result)
success = resolvedParameters[0] >= resolvedParameters[1]
- if !success {
+ if !success && !dontResolveFailedConditions {
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, ">=")
}
} else if strings.Contains(condition, ">") {
parameters, resolvedParameters := sanitizeAndResolveNumerical(strings.Split(condition, ">"), result)
success = resolvedParameters[0] > resolvedParameters[1]
- if !success {
+ if !success && !dontResolveFailedConditions {
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, ">")
}
} else if strings.Contains(condition, "<") {
parameters, resolvedParameters := sanitizeAndResolveNumerical(strings.Split(condition, "<"), result)
success = resolvedParameters[0] < resolvedParameters[1]
- if !success {
+ if !success && !dontResolveFailedConditions {
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, "<")
}
} else {
diff --git a/core/condition_bench_test.go b/core/condition_bench_test.go
index f0aec040..70e10d47 100644
--- a/core/condition_bench_test.go
+++ b/core/condition_bench_test.go
@@ -6,7 +6,7 @@ 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)
+ condition.evaluate(result, false)
}
b.ReportAllocs()
}
@@ -15,7 +15,7 @@ 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)
+ condition.evaluate(result, false)
}
b.ReportAllocs()
}
@@ -24,7 +24,7 @@ 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)
+ condition.evaluate(result, false)
}
b.ReportAllocs()
}
@@ -33,7 +33,7 @@ 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)
+ condition.evaluate(result, false)
}
b.ReportAllocs()
}
@@ -42,7 +42,7 @@ func BenchmarkCondition_evaluateWithBodyStringFailureInvalidPath(b *testing.B) {
condition := Condition("[BODY].user.name == bob.doe")
for n := 0; n < b.N; n++ {
result := &Result{body: []byte("{\"name\": \"bob.doe\"}")}
- condition.evaluate(result)
+ condition.evaluate(result, false)
}
b.ReportAllocs()
}
@@ -51,7 +51,7 @@ 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)
+ condition.evaluate(result, false)
}
b.ReportAllocs()
}
@@ -60,7 +60,7 @@ 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)
+ condition.evaluate(result, false)
}
b.ReportAllocs()
}
@@ -69,7 +69,7 @@ func BenchmarkCondition_evaluateWithStatus(b *testing.B) {
condition := Condition("[STATUS] == 200")
for n := 0; n < b.N; n++ {
result := &Result{HTTPStatus: 200}
- condition.evaluate(result)
+ condition.evaluate(result, false)
}
b.ReportAllocs()
}
@@ -78,7 +78,7 @@ func BenchmarkCondition_evaluateWithStatusFailure(b *testing.B) {
condition := Condition("[STATUS] == 200")
for n := 0; n < b.N; n++ {
result := &Result{HTTPStatus: 400}
- condition.evaluate(result)
+ condition.evaluate(result, false)
}
b.ReportAllocs()
}
diff --git a/core/condition_test.go b/core/condition_test.go
index c3697873..67935658 100644
--- a/core/condition_test.go
+++ b/core/condition_test.go
@@ -8,11 +8,12 @@ import (
func TestCondition_evaluate(t *testing.T) {
type scenario struct {
- Name string
- Condition Condition
- Result *Result
- ExpectedSuccess bool
- ExpectedOutput string
+ Name string
+ Condition Condition
+ Result *Result
+ DontResolveFailedConditions bool
+ ExpectedSuccess bool
+ ExpectedOutput string
}
scenarios := []scenario{
{
@@ -372,6 +373,14 @@ func TestCondition_evaluate(t *testing.T) {
ExpectedSuccess: false,
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",
Condition: Condition("[CONNECTED] == true"),
@@ -435,6 +444,14 @@ func TestCondition_evaluate(t *testing.T) {
ExpectedSuccess: 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",
Condition: Condition("1 == 2"),
@@ -445,7 +462,7 @@ func TestCondition_evaluate(t *testing.T) {
}
for _, scenario := range scenarios {
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 {
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) {
condition := Condition("[STATUS] ? 201")
result := &Result{HTTPStatus: 201}
- condition.evaluate(result)
+ condition.evaluate(result, false)
if result.Success {
t.Error("condition was invalid, result should've been a failure")
}
diff --git a/core/service.go b/core/service.go
index b01b7be0..d8db2f49 100644
--- a/core/service.go
+++ b/core/service.go
@@ -163,7 +163,7 @@ func (service *Service) EvaluateHealth() *Result {
result.Success = false
}
for _, condition := range service.Conditions {
- success := condition.evaluate(result)
+ success := condition.evaluate(result, service.UIConfig.DontResolveFailedConditions)
if !success {
result.Success = false
}
diff --git a/core/ui/ui.go b/core/ui/ui.go
index 75221b48..13c0f3e9 100644
--- a/core/ui/ui.go
+++ b/core/ui/ui.go
@@ -2,12 +2,16 @@ package ui
// Config is the UI configuration for services
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
func GetDefaultConfig() *Config {
return &Config{
- HideHostname: false,
+ HideHostname: false,
+ DontResolveFailedConditions: false,
}
}