From f41560cd3e24b0a114432d8d402585640044b2d9 Mon Sep 17 00:00:00 2001 From: TwinProduction Date: Tue, 14 Sep 2021 19:34:46 -0400 Subject: [PATCH] Add configuration for whether to resolve failed conditions or not --- README.md | 11 ++++++----- core/condition.go | 14 +++++++------- core/condition_bench_test.go | 18 +++++++++--------- core/condition_test.go | 31 ++++++++++++++++++++++++------- core/service.go | 2 +- core/ui/ui.go | 8 ++++++-- 6 files changed, 53 insertions(+), 31 deletions(-) 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, } }