From 4f76e13dbe6a62e394a1b3c583cd1c1d8826af0f Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 3 Oct 2024 16:48:32 +0530 Subject: [PATCH] feat: add ability to configure number of required points (#5242) --- frontend/public/locales/en-GB/alerts.json | 2 + frontend/public/locales/en/alerts.json | 2 + .../container/FormAlertRules/RuleOptions.tsx | 39 +++++++++++ frontend/src/types/api/alerts/def.ts | 2 + pkg/query-service/rules/alerting.go | 22 ++++--- pkg/query-service/rules/base_rule.go | 7 ++ pkg/query-service/rules/base_rule_test.go | 64 +++++++++++++++++++ 7 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 pkg/query-service/rules/base_rule_test.go diff --git a/frontend/public/locales/en-GB/alerts.json b/frontend/public/locales/en-GB/alerts.json index 86f21c8c78..5c5c3b851e 100644 --- a/frontend/public/locales/en-GB/alerts.json +++ b/frontend/public/locales/en-GB/alerts.json @@ -118,6 +118,8 @@ "exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.", "field_unit": "Threshold unit", "text_alert_on_absent": "Send a notification if data is missing for", + "text_require_min_points": "Run alert evaluation only when there are minimum of", + "text_num_points": "data points in each result group", "text_alert_frequency": "Run alert every", "text_for": "minutes", "selected_query_placeholder": "Select query" diff --git a/frontend/public/locales/en/alerts.json b/frontend/public/locales/en/alerts.json index 02d20a2977..3ad8390731 100644 --- a/frontend/public/locales/en/alerts.json +++ b/frontend/public/locales/en/alerts.json @@ -118,6 +118,8 @@ "exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.", "field_unit": "Threshold unit", "text_alert_on_absent": "Send a notification if data is missing for", + "text_require_min_points": "Run alert evaluation only when there are minimum of", + "text_num_points": "data points in each result group", "text_alert_frequency": "Run alert every", "text_for": "minutes", "selected_query_placeholder": "Select query" diff --git a/frontend/src/container/FormAlertRules/RuleOptions.tsx b/frontend/src/container/FormAlertRules/RuleOptions.tsx index da265f34cc..ed0792d9d5 100644 --- a/frontend/src/container/FormAlertRules/RuleOptions.tsx +++ b/frontend/src/container/FormAlertRules/RuleOptions.tsx @@ -323,6 +323,45 @@ function RuleOptions({ {t('text_for')} + + + + + { + setAlertDef({ + ...alertDef, + condition: { + ...alertDef.condition, + requireMinPoints: e.target.checked, + }, + }); + }} + /> + + {t('text_require_min_points')} + + + { + setAlertDef({ + ...alertDef, + condition: { + ...alertDef.condition, + requiredNumPoints: Number(value) || 0, + }, + }); + }} + type="number" + onWheel={(e): void => e.currentTarget.blur()} + /> + + {t('text_num_points')} + + diff --git a/frontend/src/types/api/alerts/def.ts b/frontend/src/types/api/alerts/def.ts index 9393ccd5a0..cd26d9864b 100644 --- a/frontend/src/types/api/alerts/def.ts +++ b/frontend/src/types/api/alerts/def.ts @@ -37,6 +37,8 @@ export interface RuleCondition { selectedQueryName?: string; alertOnAbsent?: boolean | undefined; absentFor?: number | undefined; + requireMinPoints?: boolean | undefined; + requiredNumPoints?: number | undefined; } export interface Labels { [key: string]: string; diff --git a/pkg/query-service/rules/alerting.go b/pkg/query-service/rules/alerting.go index ec2b0c8016..660b1896f3 100644 --- a/pkg/query-service/rules/alerting.go +++ b/pkg/query-service/rules/alerting.go @@ -106,16 +106,18 @@ const ( ) type RuleCondition struct { - CompositeQuery *v3.CompositeQuery `json:"compositeQuery,omitempty" yaml:"compositeQuery,omitempty"` - CompareOp CompareOp `yaml:"op,omitempty" json:"op,omitempty"` - Target *float64 `yaml:"target,omitempty" json:"target,omitempty"` - AlertOnAbsent bool `yaml:"alertOnAbsent,omitempty" json:"alertOnAbsent,omitempty"` - AbsentFor uint64 `yaml:"absentFor,omitempty" json:"absentFor,omitempty"` - MatchType MatchType `json:"matchType,omitempty"` - TargetUnit string `json:"targetUnit,omitempty"` - Algorithm string `json:"algorithm,omitempty"` - Seasonality string `json:"seasonality,omitempty"` - SelectedQuery string `json:"selectedQueryName,omitempty"` + CompositeQuery *v3.CompositeQuery `json:"compositeQuery,omitempty" yaml:"compositeQuery,omitempty"` + CompareOp CompareOp `yaml:"op,omitempty" json:"op,omitempty"` + Target *float64 `yaml:"target,omitempty" json:"target,omitempty"` + AlertOnAbsent bool `yaml:"alertOnAbsent,omitempty" json:"alertOnAbsent,omitempty"` + AbsentFor uint64 `yaml:"absentFor,omitempty" json:"absentFor,omitempty"` + MatchType MatchType `json:"matchType,omitempty"` + TargetUnit string `json:"targetUnit,omitempty"` + Algorithm string `json:"algorithm,omitempty"` + Seasonality string `json:"seasonality,omitempty"` + SelectedQuery string `json:"selectedQueryName,omitempty"` + RequireMinPoints bool `yaml:"requireMinPoints,omitempty" json:"requireMinPoints,omitempty"` + RequiredNumPoints int `yaml:"requiredNumPoints,omitempty" json:"requiredNumPoints,omitempty"` } func (rc *RuleCondition) GetSelectedQueryName() string { diff --git a/pkg/query-service/rules/base_rule.go b/pkg/query-service/rules/base_rule.go index 181eaa3a28..a0b402c5be 100644 --- a/pkg/query-service/rules/base_rule.go +++ b/pkg/query-service/rules/base_rule.go @@ -353,6 +353,13 @@ func (r *BaseRule) ShouldAlert(series v3.Series) (Sample, bool) { return alertSmpl, false } + if r.ruleCondition.RequireMinPoints { + if len(series.Points) < r.ruleCondition.RequiredNumPoints { + zap.L().Info("not enough data points to evaluate series, skipping", zap.String("ruleid", r.ID()), zap.Int("numPoints", len(series.Points)), zap.Int("requiredPoints", r.ruleCondition.RequiredNumPoints)) + return alertSmpl, false + } + } + switch r.matchType() { case AtleastOnce: // If any sample matches the condition, the rule is firing. diff --git a/pkg/query-service/rules/base_rule_test.go b/pkg/query-service/rules/base_rule_test.go new file mode 100644 index 0000000000..a73fa9ebea --- /dev/null +++ b/pkg/query-service/rules/base_rule_test.go @@ -0,0 +1,64 @@ +package rules + +import ( + "testing" + + v3 "go.signoz.io/signoz/pkg/query-service/model/v3" +) + +func TestBaseRule_RequireMinPoints(t *testing.T) { + threshold := 1.0 + tests := []struct { + name string + rule *BaseRule + shouldAlert bool + series *v3.Series + }{ + { + name: "test should skip if less than min points", + rule: &BaseRule{ + ruleCondition: &RuleCondition{ + RequireMinPoints: true, + RequiredNumPoints: 4, + }, + }, + series: &v3.Series{ + Points: []v3.Point{ + {Value: 1}, + {Value: 2}, + }, + }, + shouldAlert: false, + }, + { + name: "test should alert if more than min points", + rule: &BaseRule{ + ruleCondition: &RuleCondition{ + RequireMinPoints: true, + RequiredNumPoints: 4, + CompareOp: ValueIsAbove, + MatchType: AtleastOnce, + Target: &threshold, + }, + }, + series: &v3.Series{ + Points: []v3.Point{ + {Value: 1}, + {Value: 2}, + {Value: 3}, + {Value: 4}, + }, + }, + shouldAlert: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, shouldAlert := test.rule.ShouldAlert(*test.series) + if shouldAlert != test.shouldAlert { + t.Errorf("expected shouldAlert to be %v, got %v", test.shouldAlert, shouldAlert) + } + }) + } +}