-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat: Add workflow trigger for incident called 'alert_association_changed' #5254
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
096d294
d7f7b2f
67c2124
358ea1a
ef7db36
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -162,6 +162,34 @@ def insert_incident(self, tenant_id: str, incident: IncidentDto, trigger: str): | |||||||||||||||||||||||||
| for k, v in incident_enrichment.enrichments.items(): | ||||||||||||||||||||||||||
| setattr(incident, k, v) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if trigger == "alert_association_changed": | ||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||
| alerts = incident.alerts | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| processed_alerts = [] | ||||||||||||||||||||||||||
| # Iterate over the alerts and process them as needed | ||||||||||||||||||||||||||
| for alert in alerts: | ||||||||||||||||||||||||||
| # Handle multiline description | ||||||||||||||||||||||||||
| alert_description = alert.description.split("\n")[0] | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| processed_alert = f"{alert.status.capitalize()} {alert.lastReceived} [{alert.severity}] {alert_description}" | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
| processed_alert = f"{alert.status.capitalize()} {alert.lastReceived} [{alert.severity}] {alert_description}" | |
| # Standardize lastReceived to ISO 8601 with milliseconds and 'Z' | |
| last_received = alert.lastReceived | |
| if isinstance(last_received, datetime.datetime): | |
| # Ensure UTC and add 'Z' | |
| if last_received.tzinfo is not None: | |
| last_received = last_received.astimezone(datetime.timezone.utc) | |
| last_received_str = last_received.isoformat(timespec='milliseconds').replace('+00:00', 'Z') | |
| else: | |
| last_received_str = str(last_received) | |
| processed_alert = f"{alert.status.capitalize()} {last_received_str} [{alert.severity}] {alert_description}" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -766,6 +766,26 @@ def test_workflow_execution_with_disabled_workflow( | |
| "deleted incident: {{ incident.name }}" | ||
| """ | ||
|
|
||
| workflow_definition_6 = """workflow: | ||
| id: incident-triggers-test-alert-association-changed | ||
| description: test incident alert association change | ||
| triggers: | ||
| - type: incident | ||
| events: | ||
| - alert_association_changed | ||
| name: alert_association_changed | ||
| owners: [] | ||
| services: [] | ||
| steps: [] | ||
| actions: | ||
| - name: mock-action | ||
| provider: | ||
| type: console | ||
| with: | ||
| message: | | ||
| "linked_alerts: keep.len({{ incident.linked_alerts }})" | ||
| """ | ||
|
|
||
|
|
||
| @pytest.mark.timeout(15) | ||
| @pytest.mark.parametrize( | ||
|
|
@@ -792,6 +812,15 @@ def test_workflow_incident_triggers( | |
| db_session.add(workflow_created) | ||
| db_session.commit() | ||
|
|
||
| alert_1 = AlertDto( | ||
| id="alert-1", | ||
| name="Test Alert 1", | ||
| status=AlertStatus.FIRING, | ||
| severity=AlertSeverity.HIGH, | ||
| lastReceived="2025-01-30T10:00:00Z", | ||
| description="Test alert 1 description\nThis is a multiline description\nWith multiple lines of content\nTo test the split behavior" | ||
| ) | ||
|
|
||
| # Create the current alert | ||
| incident = IncidentDto( | ||
| id="ba9ddbb9-3a83-40fc-9ace-1e026e08ca2b", | ||
|
|
@@ -802,6 +831,7 @@ def test_workflow_incident_triggers( | |
| severity="critical", | ||
| is_predicted=False, | ||
| is_candidate=False, | ||
| alerts=[alert_1] | ||
| ) | ||
|
|
||
| # Insert the current alert into the workflow manager | ||
|
|
@@ -861,6 +891,31 @@ def test_workflow_incident_triggers( | |
| '"deleted incident: incident"\n' | ||
| ] | ||
|
|
||
| workflow_deleted = Workflow( | ||
| id="incident-triggers-test-alert-association-changed", | ||
| name="incident-triggers-test-alert-association-changed", | ||
| tenant_id=SINGLE_TENANT_UUID, | ||
| description="Check that incident triggers works", | ||
| created_by="[email protected]", | ||
| interval=0, | ||
| workflow_raw=workflow_definition_6, | ||
| ) | ||
| db_session.add(workflow_deleted) | ||
| db_session.commit() | ||
|
|
||
| workflow_manager.insert_incident(SINGLE_TENANT_UUID, incident, "alert_association_changed") | ||
| assert len(workflow_manager.scheduler.workflows_to_run) == 1 | ||
|
|
||
| workflow_execution_alert_association_changed = wait_for_workflow_execution( | ||
| SINGLE_TENANT_UUID, "incident-triggers-test-alert-association-changed" | ||
| ) | ||
| assert workflow_execution_alert_association_changed is not None | ||
| assert workflow_execution_alert_association_changed.status == "success" | ||
| assert workflow_execution_alert_association_changed.results["mock-action"] == [ | ||
| '"linked_alerts: 1"\n' | ||
| ] | ||
| assert len(workflow_manager.scheduler.workflows_to_run) == 0 | ||
|
|
||
|
|
||
| # @pytest.mark.parametrize( | ||
| # "test_app, test_case, alert_statuses, expected_tier, db_session", | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,11 +1,16 @@ | ||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||
| import uuid | ||||||||||||||||||||
| from datetime import datetime | ||||||||||||||||||||
| from unittest.mock import Mock, patch | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import pytest | ||||||||||||||||||||
| from fastapi import HTTPException | ||||||||||||||||||||
|
|
||||||||||||||||||||
| from keep.api.routes.workflows import get_event_from_body | ||||||||||||||||||||
| from keep.parser.parser import Parser | ||||||||||||||||||||
| from keep.api.models.alert import AlertDto, AlertSeverity, AlertStatus | ||||||||||||||||||||
| from keep.api.models.incident import IncidentDto | ||||||||||||||||||||
| from keep.api.models.db.incident import IncidentSeverity, IncidentStatus | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Assuming WorkflowParser is the class containing the get_workflow_from_dict method | ||||||||||||||||||||
| from keep.workflowmanager.workflow import Workflow | ||||||||||||||||||||
|
|
@@ -182,3 +187,102 @@ def test_handle_manual_event_workflow_test_run(): | |||||||||||||||||||
| ) | ||||||||||||||||||||
| assert workflow_scheduler.workflows_to_run[0]["test_run"] == True | ||||||||||||||||||||
| assert workflow_scheduler.workflows_to_run[0]["workflow"] == mock_workflow | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def test_insert_incident_alert_association_changed_adds_linked_alerts(): | ||||||||||||||||||||
| """Test that linked_alerts key is present when workflow trigger is alert_association_changed.""" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Create mock alerts that would be associated with the incident | ||||||||||||||||||||
| mock_alert_1 = AlertDto( | ||||||||||||||||||||
| id="alert-1", | ||||||||||||||||||||
| name="Test Alert 1", | ||||||||||||||||||||
| status=AlertStatus.FIRING, | ||||||||||||||||||||
| severity=AlertSeverity.HIGH, | ||||||||||||||||||||
| lastReceived="2025-01-30T10:00:00Z", | ||||||||||||||||||||
| description="Test alert 1 description\nThis is a multiline description\nWith multiple lines of content\nTo test the split behavior" | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| mock_alert_2 = AlertDto( | ||||||||||||||||||||
| id="alert-2", | ||||||||||||||||||||
| name="Test Alert 2", | ||||||||||||||||||||
| status=AlertStatus.RESOLVED, | ||||||||||||||||||||
| severity=AlertSeverity.CRITICAL, | ||||||||||||||||||||
| lastReceived="2025-01-30T11:00:00Z", | ||||||||||||||||||||
| description="Test alert 2 description" | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Create incident DTO with mock alerts | ||||||||||||||||||||
| incident_dto = IncidentDto( | ||||||||||||||||||||
| id=uuid.uuid4(), | ||||||||||||||||||||
| user_generated_name="Test Incident", | ||||||||||||||||||||
| alerts_count=2, | ||||||||||||||||||||
| alert_sources=["prometheus", "grafana"], | ||||||||||||||||||||
| services=["web-service"], | ||||||||||||||||||||
| severity=IncidentSeverity.HIGH, | ||||||||||||||||||||
| status=IncidentStatus.FIRING, | ||||||||||||||||||||
| is_predicted=False, | ||||||||||||||||||||
| is_candidate=False, | ||||||||||||||||||||
| creation_time=datetime.utcnow() | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Mock the alerts property to return our test alerts | ||||||||||||||||||||
| incident_dto._alerts = [mock_alert_1, mock_alert_2] | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Create a mock workflow with alert_association_changed trigger | ||||||||||||||||||||
| mock_workflow = Mock(spec=Workflow) | ||||||||||||||||||||
|
Comment on lines
+228
to
+231
|
||||||||||||||||||||
| incident_dto._alerts = [mock_alert_1, mock_alert_2] | |
| # Create a mock workflow with alert_association_changed trigger | |
| mock_workflow = Mock(spec=Workflow) | |
| with patch.object(type(incident_dto), "alerts", new_callable=property) as mock_alerts_prop: | |
| mock_alerts_prop.return_value = [mock_alert_1, mock_alert_2] | |
| # Create a mock workflow with alert_association_changed trigger | |
| mock_workflow = Mock(spec=Workflow) |
Copilot
AI
Aug 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test expects '2025-01-30T10:00:00.000Z' but the mock alert has lastReceived="2025-01-30T10:00:00Z" (without milliseconds). This mismatch will cause the test to fail.
| expected_alert_1 = "Firing 2025-01-30T10:00:00.000Z [high] Test alert 1 description" | |
| expected_alert_2 = "Resolved 2025-01-30T11:00:00.000Z [critical] Test alert 2 description" | |
| expected_alert_1 = "Firing 2025-01-30T10:00:00Z [high] Test alert 1 description" | |
| expected_alert_2 = "Resolved 2025-01-30T11:00:00Z [critical] Test alert 2 description" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
alert.status.capitalize()call will fail ifalert.statusis an enum. AlertStatus enum values need to be converted to string first before calling capitalize().