Skip to content

Commit cb3d633

Browse files
feat: Add threshold support for Correlation Rule (#4793)
1 parent b1edff0 commit cb3d633

File tree

11 files changed

+305
-21
lines changed

11 files changed

+305
-21
lines changed

keep-ui/app/(keep)/rules/CorrelationSidebar/CorrelationForm.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,41 @@ export const CorrelationForm = ({
264264
)}
265265
/>
266266
</div>
267+
268+
<div>
269+
<label
270+
className="flex items-center text-tremor-default font-medium text-tremor-content-strong"
271+
htmlFor="threshold"
272+
>
273+
Alerts threshold{" "}
274+
</label>
275+
276+
<Controller
277+
control={control}
278+
name="threshold"
279+
render={({ field: { value, onChange } }) => (
280+
<TextInput
281+
type="number"
282+
placeholder="1"
283+
className="mt-2"
284+
{...register("threshold", {
285+
required: {
286+
message: "Threshold is required",
287+
value: false,
288+
},
289+
validate: (value) => {
290+
if (value <= 0) {
291+
return "Threshold should be positive";
292+
}
293+
return true;
294+
},
295+
})}
296+
error={isSubmitted && !!get(errors, "threshold.message")}
297+
errorMessage={isSubmitted && get(errors, "threshold.message")}
298+
/>
299+
)}
300+
/>
301+
</div>
267302
</fieldset>
268303

269304
<div className="flex items-center space-x-2">

keep-ui/app/(keep)/rules/CorrelationSidebar/CorrelationSidebarBody.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export const CorrelationSidebarBody = ({
7777
incidentPrefix,
7878
multiLevel,
7979
multiLevelPropertyName,
80+
threshold,
8081
} = correlationFormData;
8182

8283
const body = {
@@ -94,6 +95,7 @@ export const CorrelationSidebarBody = ({
9495
incidentPrefix,
9596
multiLevel,
9697
multiLevelPropertyName,
98+
threshold,
9799
};
98100

99101
try {

keep-ui/app/(keep)/rules/CorrelationSidebar/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const DEFAULT_CORRELATION_FORM_VALUES: CorrelationFormType = {
2727
incidentPrefix: "",
2828
multiLevel: false,
2929
multiLevelPropertyName: "",
30+
threshold: 1,
3031
query: {
3132
combinator: "or",
3233
rules: [
@@ -92,6 +93,7 @@ export const CorrelationSidebar = ({
9293
incidentPrefix: selectedRule.incident_prefix || "",
9394
multiLevel: selectedRule.multi_level,
9495
multiLevelPropertyName: selectedRule.multi_level_property_name || "",
96+
threshold: selectedRule.threshold || 1,
9597
};
9698
}
9799

keep-ui/app/(keep)/rules/CorrelationSidebar/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ export type CorrelationFormType = {
1414
incidentPrefix: string;
1515
multiLevel: boolean;
1616
multiLevelPropertyName?: string;
17+
threshold: number;
1718
};

keep-ui/utils/hooks/useRules.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export type Rule = {
2727
incident_prefix: string | null;
2828
multi_level: boolean;
2929
multi_level_property_name: string | null;
30+
threshold: number;
3031
};
3132

3233
export const useRules = (options?: SWRConfiguration) => {

keep/api/core/db.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2156,6 +2156,7 @@ def create_rule(
21562156
incident_prefix=None,
21572157
multi_level=False,
21582158
multi_level_property_name=None,
2159+
threshold=1,
21592160
):
21602161
grouping_criteria = grouping_criteria or []
21612162
with Session(engine) as session:
@@ -2177,6 +2178,7 @@ def create_rule(
21772178
incident_prefix=incident_prefix,
21782179
multi_level=multi_level,
21792180
multi_level_property_name=multi_level_property_name,
2181+
threshold=threshold,
21802182
)
21812183
session.add(rule)
21822184
session.commit()
@@ -2201,6 +2203,7 @@ def update_rule(
22012203
incident_prefix,
22022204
multi_level,
22032205
multi_level_property_name,
2206+
threshold,
22042207
):
22052208
rule_uuid = __convert_to_uuid(rule_id)
22062209
if not rule_uuid:
@@ -2227,6 +2230,7 @@ def update_rule(
22272230
rule.incident_prefix = incident_prefix
22282231
rule.multi_level = multi_level
22292232
rule.multi_level_property_name = multi_level_property_name
2233+
rule.threshold = threshold
22302234
session.commit()
22312235
session.refresh(rule)
22322236
return rule
@@ -2338,7 +2342,7 @@ def create_incident_for_grouping_rule(
23382342
rule_fingerprint=rule_fingerprint,
23392343
is_predicted=True,
23402344
is_candidate=rule.require_approve,
2341-
is_visible=rule.create_on == CreateIncidentOn.ANY.value,
2345+
is_visible=False,# rule.create_on == CreateIncidentOn.ANY.value,
23422346
incident_type=IncidentType.RULE.value,
23432347
same_incident_in_the_past_id=past_incident.id if past_incident else None,
23442348
resolve_on=rule.resolve_on,
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""Add threshold field to Rule
2+
3+
Revision ID: fcef2c58b21c
4+
Revises: 7b687c555318
5+
Create Date: 2025-05-15 00:34:31.753003
6+
7+
"""
8+
9+
import sqlalchemy as sa
10+
from alembic import op
11+
12+
# revision identifiers, used by Alembic.
13+
revision = "fcef2c58b21c"
14+
down_revision = "7b687c555318"
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade() -> None:
20+
21+
with op.batch_alter_table("rule", schema=None) as batch_op:
22+
batch_op.add_column(sa.Column("threshold", sa.Integer(), nullable=False, server_default="1"))
23+
batch_op.create_check_constraint("rule_threshold_positive_int_constraint", "threshold>0")
24+
25+
26+
def downgrade() -> None:
27+
28+
with op.batch_alter_table("rule", schema=None) as batch_op:
29+
batch_op.drop_constraint("rule_threshold_positive_int_constraint", type_="check")
30+
batch_op.drop_column("threshold")

keep/api/models/db/rule.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from enum import Enum
33
from uuid import UUID, uuid4
44

5+
from sqlalchemy import CheckConstraint
56
from sqlmodel import JSON, Column, Field, SQLModel
67

78
# Currently a rule_definition is a list of SQL expressions
@@ -55,3 +56,4 @@ class Rule(SQLModel, table=True):
5556
incident_prefix: str | None = None
5657
multi_level: bool = False
5758
multi_level_property_name: str | None = None
59+
threshold: int = Field(sa_column_args=(CheckConstraint("threshold>0"),), default=1)

keep/api/routes/rules.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class RuleCreateDto(BaseModel):
3434
incidentPrefix: str = None
3535
multiLevel: bool = False
3636
multiLevelPropertyName: str = None
37+
threshold: int = 1
3738

3839

3940
@router.get(
@@ -92,6 +93,7 @@ async def create_rule(
9293
incident_prefix = rule_create_request.incidentPrefix
9394
multi_level = rule_create_request.multiLevel
9495
multi_level_property_name = rule_create_request.multiLevelPropertyName
96+
threshold = rule_create_request.threshold
9597

9698
if not sql:
9799
raise HTTPException(status_code=400, detail="SQL is required")
@@ -118,6 +120,9 @@ async def create_rule(
118120
if not create_on:
119121
raise HTTPException(status_code=400, detail="createOn is required")
120122

123+
if not threshold:
124+
raise HTTPException(status_code=400, detail="threshold is required")
125+
121126
rule = create_rule_db(
122127
tenant_id=tenant_id,
123128
name=rule_name,
@@ -138,6 +143,7 @@ async def create_rule(
138143
incident_prefix=incident_prefix,
139144
multi_level=multi_level,
140145
multi_level_property_name=multi_level_property_name,
146+
threshold=threshold,
141147
)
142148
logger.info("Rule created")
143149
return rule
@@ -193,6 +199,7 @@ async def update_rule(
193199
incident_prefix = body.get("incidentPrefix", None)
194200
multi_level = body.get("multiLevel", False)
195201
multi_level_property_name = body.get("multiLevelPropertyName", None)
202+
threshold = body.get("threshold", 1)
196203
except Exception:
197204
raise HTTPException(status_code=400, detail="Invalid request body")
198205

@@ -225,6 +232,9 @@ async def update_rule(
225232
if not create_on:
226233
raise HTTPException(status_code=400, detail="createOn is required")
227234

235+
if not threshold:
236+
raise HTTPException(status_code=400, detail="threshold is required")
237+
228238
rule = update_rule_db(
229239
tenant_id=tenant_id,
230240
rule_id=rule_id,
@@ -245,6 +255,7 @@ async def update_rule(
245255
incident_prefix=incident_prefix,
246256
multi_level=multi_level,
247257
multi_level_property_name=multi_level_property_name,
258+
threshold=threshold,
248259
)
249260

250261
if rule:

keep/rulesengine/rulesengine.py

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -138,22 +138,24 @@ def _run_cel_rules(
138138
rule_groups = self._extract_subrules(
139139
rule.definition_cel
140140
)
141-
142-
if not rule.require_approve:
143-
if rule.create_on == "any" or (
144-
rule.create_on == "all"
145-
and len(rule_groups) == len(matched_rules)
146-
):
147-
self.logger.info(
148-
"Single event is enough, so creating incident"
149-
)
150-
incident.is_visible = True
151-
elif rule.create_on == "all":
152-
incident = (
153-
self._process_event_for_history_based_rule(
154-
incident, rule, session
141+
firing_count = sum([alert.event.get("firingCounter", 1) for alert in incident.alerts])
142+
alerts_count = max(incident.alerts_count, firing_count)
143+
if alerts_count >= rule.threshold:
144+
if not rule.require_approve:
145+
if rule.create_on == "any" or (
146+
rule.create_on == "all"
147+
and len(rule_groups) == len(matched_rules)
148+
):
149+
self.logger.info(
150+
"Single event is enough, so creating incident"
151+
)
152+
incident.is_visible = True
153+
elif rule.create_on == "all":
154+
incident = (
155+
self._process_event_for_history_based_rule(
156+
incident, rule, session
157+
)
155158
)
156-
)
157159

158160
send_created_event = incident.is_visible
159161

@@ -266,9 +268,9 @@ def _get_or_create_incident(
266268
},
267269
)
268270
alerts = existed_incident.alerts
269-
vairables = self.get_vaiables(rule.incident_name_template)
271+
variables = self.get_vaiables(rule.incident_name_template)
270272
values = set()
271-
for var in vairables:
273+
for var in variables:
272274
var_to_replace = ""
273275
alerts_dtos = convert_db_alerts_to_dto_alerts(alerts)
274276
for alert in alerts_dtos:
@@ -306,13 +308,13 @@ def _get_or_create_incident(
306308
if event.status == AlertStatus.FIRING.value:
307309
if rule.incident_name_template:
308310
incident_name = copy.copy(rule.incident_name_template)
309-
vairables = self.get_vaiables(rule.incident_name_template)
310-
if not vairables:
311+
variables = self.get_vaiables(rule.incident_name_template)
312+
if not variables:
311313
self.logger.warning(
312314
f"Failed to fetch the appropriate labels from the event {event.id} and rule {rule.name}"
313315
)
314316
incident_name = None
315-
for var in vairables:
317+
for var in variables:
316318
value = self.get_value_from_event(event, var)
317319
pattern = r"\{\{\s*" + re.escape(var) + r"\s*\}\}"
318320
incident_name = re.sub(pattern, value, incident_name)

0 commit comments

Comments
 (0)