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)
+ }
+ })
+ }
+}