From f34fa5ccb05142f92d40b9f0e5193412759fd951 Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Tue, 12 Nov 2024 22:12:57 +0400 Subject: [PATCH 01/26] Add LastAlert and LastAlertToIncident and adjust incidents logic to it --- keep/api/core/db.py | 338 ++++++++++-------- keep/api/models/db/alert.py | 96 ++--- .../versions/2024-11-05-22-48_bdae8684d0b4.py | 180 ++++++++++ keep/api/routes/incidents.py | 2 +- keep/api/tasks/process_event_task.py | 6 +- keep/api/utils/enrichment_helpers.py | 86 ++--- keep/rulesengine/rulesengine.py | 4 +- tests/conftest.py | 5 + tests/test_incidents.py | 191 ++++++---- tests/test_metrics.py | 2 +- tests/test_rules_engine.py | 28 +- 11 files changed, 631 insertions(+), 307 deletions(-) create mode 100644 keep/api/models/db/migrations/versions/2024-11-05-22-48_bdae8684d0b4.py diff --git a/keep/api/core/db.py b/keep/api/core/db.py index cc9294f26..f4a9a1455 100644 --- a/keep/api/core/db.py +++ b/keep/api/core/db.py @@ -16,6 +16,7 @@ from uuid import uuid4 import validators +from dateutil.tz import tz from dotenv import find_dotenv, load_dotenv from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor from sqlalchemy import ( @@ -44,8 +45,8 @@ # This import is required to create the tables from keep.api.models.ai_external import ( - ExternalAIConfigAndMetadata, - ExternalAIConfigAndMetadataDto, + ExternalAIConfigAndMetadata, + ExternalAIConfigAndMetadataDto, ) from keep.api.models.alert import ( AlertStatus, @@ -1741,9 +1742,10 @@ def get_incident_for_grouping_rule( # if the last alert in the incident is older than the timeframe, create a new incident is_incident_expired = False - if incident and incident.alerts: + if incident and incident.alerts_count > 0: + enrich_incidents_with_alerts(tenant_id, [incident], session) is_incident_expired = max( - alert.timestamp for alert in incident.alerts + alert.timestamp for alert in incident._alerts ) < datetime.utcnow() - timedelta(seconds=timeframe) # if there is no incident with the rule_fingerprint, create it or existed is already expired @@ -2824,12 +2826,12 @@ def update_preset_options(tenant_id: str, preset_id: str, options: dict) -> Pres def assign_alert_to_incident( - alert_id: UUID | str, + fingerprint: str, incident: Incident, tenant_id: str, session: Optional[Session] = None, ): - return add_alerts_to_incident(tenant_id, incident, [alert_id], session=session) + return add_alerts_to_incident(tenant_id, incident, [fingerprint], session=session) def is_alert_assigned_to_incident( @@ -3106,6 +3108,32 @@ def filter_query(session: Session, query, field, value): return query +def enrich_incidents_with_alerts(tenant_id: str, incidents: List[Incident], session: Optional[Session]=None): + with existed_or_new_session(session) as session: + incident_alerts = session.exec( + select(LastAlertToIncident.incident_id, Alert) + .select_from(LastAlert) + .join(LastAlertToIncident, and_( + LastAlertToIncident.fingerprint == LastAlert.fingerprint, + LastAlertToIncident.deleted_at == NULL_FOR_DELETED_AT, + )) + .join(Alert, LastAlert.alert_id == Alert.id) + .where( + LastAlert.tenant_id == tenant_id, + LastAlertToIncident.incident_id.in_([incident.id for incident in incidents]) + ) + ).all() + + alerts_per_incident = defaultdict(list) + for incident_id, alert in incident_alerts: + alerts_per_incident[incident_id].append(alert) + + for incident in incidents: + incident._alerts = alerts_per_incident[incident.id] + + return incidents + + def get_last_incidents( tenant_id: str, limit: int = 25, @@ -3147,9 +3175,6 @@ def get_last_incidents( if allowed_incident_ids: query = query.filter(Incident.id.in_(allowed_incident_ids)) - if with_alerts: - query = query.options(joinedload(Incident.alerts)) - if is_predicted is not None: query = query.filter(Incident.is_predicted == is_predicted) @@ -3181,23 +3206,30 @@ def get_last_incidents( # Execute the query incidents = query.all() + if with_alerts: + enrich_incidents_with_alerts(tenant_id, incidents, session) + return incidents, total_count def get_incident_by_id( - tenant_id: str, incident_id: str | UUID, with_alerts: bool = False + tenant_id: str, incident_id: str | UUID, with_alerts: bool = False, + session: Optional[Session] = None, ) -> Optional[Incident]: - with Session(engine) as session: + with existed_or_new_session(session) as session: query = session.query( Incident, ).filter( Incident.tenant_id == tenant_id, Incident.id == incident_id, ) + incident = query.first() if with_alerts: - query = query.options(joinedload(Incident.alerts)) + enrich_incidents_with_alerts( + tenant_id, [incident], session, + ) - return query.first() + return incident def create_incident_from_dto( @@ -3254,7 +3286,6 @@ def create_incident_from_dict( session.add(new_incident) session.commit() session.refresh(new_incident) - new_incident.alerts = [] return new_incident @@ -3366,43 +3397,24 @@ def get_incident_alerts_and_links_by_incident_id( ) -> tuple[List[tuple[Alert, AlertToIncident]], int]: with existed_or_new_session(session) as session: - last_fingerprints_subquery = ( - session.query( - Alert.fingerprint, func.max(Alert.timestamp).label("max_timestamp") - ) - .join(AlertToIncident, AlertToIncident.alert_id == Alert.id) - .filter( - AlertToIncident.tenant_id == tenant_id, - AlertToIncident.incident_id == incident_id, - ) - .group_by(Alert.fingerprint) - .subquery() - ) - query = ( session.query( Alert, - AlertToIncident, + LastAlertToIncident, ) - .select_from(last_fingerprints_subquery) - .outerjoin( - Alert, - and_( - last_fingerprints_subquery.c.fingerprint == Alert.fingerprint, - last_fingerprints_subquery.c.max_timestamp == Alert.timestamp, - ), - ) - .join(AlertToIncident, AlertToIncident.alert_id == Alert.id) + .select_from(LastAlertToIncident) + .join(LastAlert, LastAlert.fingerprint == LastAlertToIncident.fingerprint) + .join(Alert, LastAlert.alert_id == Alert.id) .filter( - AlertToIncident.tenant_id == tenant_id, - AlertToIncident.incident_id == incident_id, + LastAlertToIncident.tenant_id == tenant_id, + LastAlertToIncident.incident_id == incident_id, ) - .order_by(col(Alert.timestamp).desc()) + .order_by(col(LastAlert.timestamp).desc()) .options(joinedload(Alert.alert_enrichment)) ) if not include_unlinked: query = query.filter( - AlertToIncident.deleted_at == NULL_FOR_DELETED_AT, + LastAlertToIncident.deleted_at == NULL_FOR_DELETED_AT, ) total_count = query.count() @@ -3464,8 +3476,7 @@ def get_all_same_alert_ids( def get_alerts_data_for_incident( tenant_id: str, - alert_ids: List[str | UUID], - existed_fingerprints: Optional[List[str]] = None, + fingerprints: Optional[List[str]] = None, session: Optional[Session] = None, ) -> dict: """ @@ -3479,8 +3490,6 @@ def get_alerts_data_for_incident( Returns: dict {sources: list[str], services: list[str], count: int} """ - existed_fingerprints = existed_fingerprints or [] - with existed_or_new_session(session) as session: fields = ( @@ -3491,16 +3500,18 @@ def get_alerts_data_for_incident( ) alerts_data = session.exec( - select(*fields).where( - Alert.tenant_id == tenant_id, - col(Alert.id).in_(alert_ids), + select(*fields) + .select_from(LastAlert) + .join(Alert, LastAlert.alert_id == Alert.id) + .where( + LastAlert.tenant_id == tenant_id, + col(LastAlert.fingerprint).in_(fingerprints), ) ).all() sources = [] services = [] severities = [] - fingerprints = set() for service, source, fingerprint, severity in alerts_data: if source: @@ -3512,21 +3523,19 @@ def get_alerts_data_for_incident( severities.append(IncidentSeverity.from_number(severity)) else: severities.append(IncidentSeverity(severity)) - if fingerprint and fingerprint not in existed_fingerprints: - fingerprints.add(fingerprint) return { "sources": set(sources), "services": set(services), "max_severity": max(severities), - "count": len(fingerprints), + "count": len(alerts_data), } def add_alerts_to_incident_by_incident_id( tenant_id: str, incident_id: str | UUID, - alert_ids: List[UUID], + fingerprints: List[str], is_created_by_ai: bool = False, session: Optional[Session] = None, ) -> Optional[Incident]: @@ -3540,62 +3549,52 @@ def add_alerts_to_incident_by_incident_id( if not incident: return None return add_alerts_to_incident( - tenant_id, incident, alert_ids, is_created_by_ai, session + tenant_id, incident, fingerprints, is_created_by_ai, session ) def add_alerts_to_incident( tenant_id: str, incident: Incident, - alert_ids: List[UUID], + fingerprints: List[str], is_created_by_ai: bool = False, session: Optional[Session] = None, override_count: bool = False, ) -> Optional[Incident]: logger.info( - f"Adding alerts to incident {incident.id} in database, total {len(alert_ids)} alerts", + f"Adding alerts to incident {incident.id} in database, total {len(fingerprints)} alerts", extra={"tags": {"tenant_id": tenant_id, "incident_id": incident.id}}, ) with existed_or_new_session(session) as session: with session.no_autoflush: - all_alert_ids = get_all_same_alert_ids(tenant_id, alert_ids, session) # Use a set for faster membership checks - existing_alert_ids = set( - session.exec( - select(AlertToIncident.alert_id).where( - AlertToIncident.deleted_at == NULL_FOR_DELETED_AT, - AlertToIncident.tenant_id == tenant_id, - AlertToIncident.incident_id == incident.id, - col(AlertToIncident.alert_id).in_(all_alert_ids), - ) - ).all() - ) + existing_fingerprints = set( session.exec( - select(Alert.fingerprint) - .join(AlertToIncident, AlertToIncident.alert_id == Alert.id) + select(LastAlert.fingerprint) + .join(LastAlertToIncident, LastAlertToIncident.fingerprint == LastAlert.fingerprint) .where( - AlertToIncident.deleted_at == NULL_FOR_DELETED_AT, - AlertToIncident.tenant_id == tenant_id, - AlertToIncident.incident_id == incident.id, + LastAlertToIncident.deleted_at == NULL_FOR_DELETED_AT, + LastAlertToIncident.tenant_id == tenant_id, + LastAlertToIncident.incident_id == incident.id, ) ).all() ) - new_alert_ids = [ - alert_id - for alert_id in all_alert_ids - if alert_id not in existing_alert_ids - ] + new_fingerprints = { + fingerprint + for fingerprint in fingerprints + if fingerprint not in existing_fingerprints + } - if not new_alert_ids: + if not new_fingerprints: return incident alerts_data_for_incident = get_alerts_data_for_incident( - tenant_id, new_alert_ids, existing_fingerprints, session + tenant_id, new_fingerprints, session ) incident.sources = list( @@ -3617,13 +3616,13 @@ def add_alerts_to_incident( else: incident.alerts_count = alerts_data_for_incident["count"] alert_to_incident_entries = [ - AlertToIncident( - alert_id=alert_id, + LastAlertToIncident( + fingerprint=fingerprint, incident_id=incident.id, tenant_id=tenant_id, is_created_by_ai=is_created_by_ai, ) - for alert_id in new_alert_ids + for fingerprint in new_fingerprints ] for idx, entry in enumerate(alert_to_incident_entries): @@ -3640,11 +3639,11 @@ def add_alerts_to_incident( started_at, last_seen_at = session.exec( select(func.min(Alert.timestamp), func.max(Alert.timestamp)) - .join(AlertToIncident, AlertToIncident.alert_id == Alert.id) + .join(LastAlertToIncident, LastAlertToIncident.fingerprint == Alert.fingerprint) .where( - AlertToIncident.deleted_at == NULL_FOR_DELETED_AT, - AlertToIncident.tenant_id == tenant_id, - AlertToIncident.incident_id == incident.id, + LastAlertToIncident.deleted_at == NULL_FOR_DELETED_AT, + LastAlertToIncident.tenant_id == tenant_id, + LastAlertToIncident.incident_id == incident.id, ) ).one() @@ -3661,12 +3660,11 @@ def get_incident_unique_fingerprint_count(tenant_id: str, incident_id: str) -> i with Session(engine) as session: return session.execute( select(func.count(1)) - .select_from(AlertToIncident) - .join(Alert, AlertToIncident.alert_id == Alert.id) + .select_from(LastAlertToIncident) .where( - AlertToIncident.deleted_at == NULL_FOR_DELETED_AT, - Alert.tenant_id == tenant_id, - AlertToIncident.incident_id == incident_id, + LastAlertToIncident.deleted_at == NULL_FOR_DELETED_AT, + LastAlertToIncident.tenant_id == tenant_id, + LastAlertToIncident.incident_id == incident_id, ) ).scalar() @@ -3678,12 +3676,14 @@ def get_last_alerts_for_incidents( query = ( session.query( Alert, - AlertToIncident.incident_id, + LastAlertToIncident.incident_id, ) - .join(AlertToIncident, Alert.id == AlertToIncident.alert_id) + .select_from(LastAlert) + .join(LastAlertToIncident, LastAlert.fingerprint == LastAlertToIncident.fingerprint) + .join(Alert, LastAlert.alert_id == Alert.id) .filter( - AlertToIncident.deleted_at == NULL_FOR_DELETED_AT, - AlertToIncident.incident_id.in_(incident_ids), + LastAlertToIncident.deleted_at == NULL_FOR_DELETED_AT, + LastAlertToIncident.incident_id.in_(incident_ids), ) .order_by(Alert.timestamp.desc()) ) @@ -3698,7 +3698,7 @@ def get_last_alerts_for_incidents( def remove_alerts_to_incident_by_incident_id( - tenant_id: str, incident_id: str | UUID, alert_ids: List[UUID] + tenant_id: str, incident_id: str | UUID, fingerprints: List[str] ) -> Optional[int]: with Session(engine) as session: incident = session.exec( @@ -3711,16 +3711,14 @@ def remove_alerts_to_incident_by_incident_id( if not incident: return None - all_alert_ids = get_all_same_alert_ids(tenant_id, alert_ids, session) - # Removing alerts-to-incident relation for provided alerts_ids deleted = ( - session.query(AlertToIncident) + session.query(LastAlertToIncident) .where( - AlertToIncident.deleted_at == NULL_FOR_DELETED_AT, - AlertToIncident.tenant_id == tenant_id, - AlertToIncident.incident_id == incident.id, - col(AlertToIncident.alert_id).in_(all_alert_ids), + LastAlertToIncident.deleted_at == NULL_FOR_DELETED_AT, + LastAlertToIncident.tenant_id == tenant_id, + LastAlertToIncident.incident_id == incident.id, + col(LastAlertToIncident.fingerprint).in_(fingerprints), ) .update( { @@ -3732,7 +3730,7 @@ def remove_alerts_to_incident_by_incident_id( # Getting aggregated data for incidents for alerts which just was removed alerts_data_for_incident = get_alerts_data_for_incident( - tenant_id, all_alert_ids, session=session + tenant_id, fingerprints, session=session ) service_field = get_json_extract_field(session, Alert.event, "service") @@ -3741,10 +3739,12 @@ def remove_alerts_to_incident_by_incident_id( # which still assigned with the incident existed_services_query = ( select(func.distinct(service_field)) - .join(AlertToIncident, Alert.id == AlertToIncident.alert_id) + .select_from(LastAlert) + .join(LastAlertToIncident, LastAlert.fingerprint == LastAlertToIncident.fingerprint) + .join(Alert, LastAlert.alert_id == Alert.id) .filter( - AlertToIncident.deleted_at == NULL_FOR_DELETED_AT, - AlertToIncident.incident_id == incident_id, + LastAlertToIncident.deleted_at == NULL_FOR_DELETED_AT, + LastAlertToIncident.incident_id == incident_id, service_field.in_(alerts_data_for_incident["services"]), ) ) @@ -3754,7 +3754,9 @@ def remove_alerts_to_incident_by_incident_id( # which still assigned with the incident existed_sources_query = ( select(col(Alert.provider_type).distinct()) - .join(AlertToIncident, Alert.id == AlertToIncident.alert_id) + .select_from(LastAlert) + .join(LastAlertToIncident, LastAlert.fingerprint == LastAlertToIncident.fingerprint) + .join(Alert, LastAlert.alert_id == Alert.id) .filter( AlertToIncident.deleted_at == NULL_FOR_DELETED_AT, AlertToIncident.incident_id == incident_id, @@ -3777,10 +3779,12 @@ def remove_alerts_to_incident_by_incident_id( started_at, last_seen_at = session.exec( select(func.min(Alert.timestamp), func.max(Alert.timestamp)) - .join(AlertToIncident, AlertToIncident.alert_id == Alert.id) + .select_from(LastAlert) + .join(LastAlertToIncident, LastAlert.fingerprint == LastAlertToIncident.fingerprint) + .join(Alert, LastAlert.alert_id == Alert.id) .where( - AlertToIncident.tenant_id == tenant_id, - AlertToIncident.incident_id == incident.id, + LastAlertToIncident.tenant_id == tenant_id, + LastAlertToIncident.incident_id == incident.id, ) ).one() @@ -3822,7 +3826,6 @@ def merge_incidents_to_id( .where( Incident.tenant_id == tenant_id, Incident.id == destination_incident_id ) - .options(joinedload(Incident.alerts)) ).first() if not destination_incident: @@ -3837,12 +3840,14 @@ def merge_incidents_to_id( ) ).all() + enrich_incidents_with_alerts(tenant_id, source_incidents, session=session) + merged_incident_ids = [] skipped_incident_ids = [] failed_incident_ids = [] for source_incident in source_incidents: - source_incident_alerts_ids = [alert.id for alert in source_incident.alerts] - if not source_incident_alerts_ids: + source_incident_alerts_fingerprints = [alert.fingerprint for alert in source_incident._alerts] + if not source_incident_alerts_fingerprints: logger.info(f"Source incident {source_incident.id} doesn't have alerts") skipped_incident_ids.append(source_incident.id) continue @@ -3854,7 +3859,7 @@ def merge_incidents_to_id( remove_alerts_to_incident_by_incident_id( tenant_id, source_incident.id, - [alert.id for alert in source_incident.alerts], + [alert.fingerprint for alert in source_incident._alerts], ) except OperationalError as e: logger.error( @@ -3864,7 +3869,7 @@ def merge_incidents_to_id( add_alerts_to_incident( tenant_id, destination_incident, - source_incident_alerts_ids, + source_incident_alerts_fingerprints, session=session, ) merged_incident_ids.append(source_incident.id) @@ -4270,17 +4275,16 @@ def is_all_incident_alerts_resolved( enriched_status_field.label("enriched_status"), status_field.label("status"), ) - .select_from(Alert) + .select_from(LastAlert) + .join(Alert, LastAlert.alert_id == Alert.id) .outerjoin( AlertEnrichment, Alert.fingerprint == AlertEnrichment.alert_fingerprint ) - .join(AlertToIncident, AlertToIncident.alert_id == Alert.id) + .join(LastAlertToIncident, LastAlertToIncident.fingerprint == LastAlert.fingerprint) .where( - AlertToIncident.deleted_at == NULL_FOR_DELETED_AT, - AlertToIncident.incident_id == incident.id, + LastAlertToIncident.deleted_at == NULL_FOR_DELETED_AT, + LastAlertToIncident.incident_id == incident.id, ) - .group_by(Alert.fingerprint) - .having(func.max(Alert.timestamp)) ).subquery() not_resolved_exists = session.query( @@ -4337,8 +4341,8 @@ def is_edge_incident_alert_resolved( .outerjoin( AlertEnrichment, Alert.fingerprint == AlertEnrichment.alert_fingerprint ) - .join(AlertToIncident, AlertToIncident.alert_id == Alert.id) - .where(AlertToIncident.incident_id == incident.id) + .join(LastAlertToIncident, LastAlertToIncident.fingerprint == Alert.fingerprint) + .where(LastAlertToIncident.incident_id == incident.id) .group_by(Alert.fingerprint) .having(func.max(Alert.timestamp)) .order_by(direction(Alert.timestamp)) @@ -4504,28 +4508,28 @@ def get_resource_ids_by_resource_type( result = session.exec(query) return result.all() - -def get_or_creat_posthog_instance_id(session: Optional[Session] = None): - POSTHOG_INSTANCE_ID_KEY = "posthog_instance_id" - with Session(engine) as session: - system = session.exec( - select(System).where(System.name == POSTHOG_INSTANCE_ID_KEY) - ).first() - if system: +def get_or_creat_posthog_instance_id( + session: Optional[Session] = None + ): + POSTHOG_INSTANCE_ID_KEY = "posthog_instance_id" + with Session(engine) as session: + system = session.exec(select(System).where(System.name == POSTHOG_INSTANCE_ID_KEY)).first() + if system: + return system.value + + system = System( + id=str(uuid4()), + name=POSTHOG_INSTANCE_ID_KEY, + value=str(uuid4()), + ) + session.add(system) + session.commit() + session.refresh(system) return system.value - system = System( - id=str(uuid4()), - name=POSTHOG_INSTANCE_ID_KEY, - value=str(uuid4()), - ) - session.add(system) - session.commit() - session.refresh(system) - return system.value - - -def get_activity_report(session: Optional[Session] = None): +def get_activity_report( + session: Optional[Session] = None + ): from keep.api.models.db.user import User last_24_hours = datetime.utcnow() - timedelta(hours=24) @@ -4553,8 +4557,46 @@ def get_activity_report(session: Optional[Session] = None): ) activity_report["last_24_hours_workflows_executed"] = ( session.query(WorkflowExecution) - .filter(WorkflowExecution.started >= last_24_hours) - .count() - ) - + .filter(WorkflowExecution.started >= last_24_hours).count() +) return activity_report + + +def get_last_alert_by_fingerprint( + tenant_id: str, fingerprint: str, session: Optional[Session] = None +) -> Optional[Alert]: + with existed_or_new_session(session) as session: + return session.exec( + select(LastAlert) + .where( + and_( + LastAlert.tenant_id == tenant_id, + LastAlert.fingerprint == fingerprint, + ) + ) + ).first() + +def set_last_alert( + tenant_id: str, alert: Alert, session: Optional[Session] = None +) -> None: + last_alert = get_last_alert_by_fingerprint(tenant_id, alert.fingerprint, session) + + # To prevent rare, but possible race condition + # For example if older alert failed to process + # and retried after new one + + if last_alert and last_alert.timestamp.replace(tzinfo=tz.UTC) < alert.timestamp.replace(tzinfo=tz.UTC): + last_alert.timestamp = alert.timestamp + last_alert.alert_id = alert.id + session.add(last_alert) + session.commit() + + elif not last_alert: + last_alert = LastAlert( + tenant_id=tenant_id, + fingerprint=alert.fingerprint, + timestamp=alert.timestamp, + alert_id=alert.id, + ) + session.add(last_alert) + session.commit() diff --git a/keep/api/models/db/alert.py b/keep/api/models/db/alert.py index 11efa9ef7..86449a5a5 100644 --- a/keep/api/models/db/alert.py +++ b/keep/api/models/db/alert.py @@ -4,12 +4,13 @@ from typing import List, Optional from uuid import UUID, uuid4 +from pydantic import PrivateAttr from sqlalchemy import ForeignKey, UniqueConstraint from sqlalchemy.dialects.mssql import DATETIME2 as MSSQL_DATETIME2 from sqlalchemy.dialects.mysql import DATETIME as MySQL_DATETIME from sqlalchemy.engine.url import make_url from sqlalchemy_utils import UUIDType -from sqlmodel import JSON, TEXT, Column, DateTime, Field, Index, Relationship, SQLModel +from sqlmodel import JSON, TEXT, Column, DateTime, Field, Index, Relationship, SQLModel, Session from keep.api.consts import RUNNING_IN_CLOUD_RUN from keep.api.core.config import config @@ -60,8 +61,8 @@ class AlertToIncident(SQLModel, table=True): primary_key=True, ) ) - alert: "Alert" = Relationship(back_populates="alert_to_incident_link") - incident: "Incident" = Relationship(back_populates="alert_to_incident_link") + # alert: "Alert" = Relationship(back_populates="alert_to_incident_link") + # incident: "Incident" = Relationship(back_populates="alert_to_incident_link") is_created_by_ai: bool = Field(default=False) @@ -72,6 +73,49 @@ class AlertToIncident(SQLModel, table=True): default=NULL_FOR_DELETED_AT, ) +class LastAlert(SQLModel, table=True): + + tenant_id: str = Field(foreign_key="tenant.id", nullable=False) + fingerprint: str = Field(primary_key=True) + alert_id: UUID = Field(foreign_key="alert.id") + timestamp: datetime = Field(nullable=False, index=True) + + +class LastAlertToIncident(SQLModel, table=True): + tenant_id: str = Field(foreign_key="tenant.id", nullable=False) + timestamp: datetime = Field(default_factory=datetime.utcnow) + + fingerprint: str = Field(foreign_key="lastalert.fingerprint", primary_key=True) + incident_id: UUID = Field( + sa_column=Column( + UUIDType(binary=False), + ForeignKey("incident.id", ondelete="CASCADE"), + primary_key=True, + ) + ) + + is_created_by_ai: bool = Field(default=False) + + deleted_at: datetime = Field( + default_factory=None, + nullable=True, + primary_key=True, + default=NULL_FOR_DELETED_AT, + ) + + # alert: "Alert" = Relationship( + # back_populates="alert_to_incident_link", + # sa_relationship = relationship( + # "Alert", + # secondary="lastalert", + # primaryjoin=f"""LastAlertToIncident.fingerprint == LastAlert.fingerprint""", + # secondaryjoin="LastAlert.alert_id == Alert.id", + # overlaps="alert,lastalert", + # viewonly=True, + # ), + # ) + # incident: "Incident" = Relationship(back_populates="alert_to_incident_link") + class Incident(SQLModel, table=True): id: UUID = Field(default_factory=uuid4, primary_key=True) @@ -96,25 +140,10 @@ class Incident(SQLModel, table=True): end_time: datetime | None last_seen_time: datetime | None - # map of attributes to values - alerts: List["Alert"] = Relationship( - back_populates="incidents", - link_model=AlertToIncident, - # primaryjoin is used to filter out deleted links for various DB dialects - sa_relationship_kwargs={ - "primaryjoin": f"""and_(AlertToIncident.incident_id == Incident.id, - or_( - AlertToIncident.deleted_at == '{NULL_FOR_DELETED_AT.strftime('%Y-%m-%d %H:%M:%S.%f')}', - AlertToIncident.deleted_at == '{NULL_FOR_DELETED_AT.strftime('%Y-%m-%d %H:%M:%S')}' - ))""", - "uselist": True, - "overlaps": "alert,incident", - }, - ) - alert_to_incident_link: List[AlertToIncident] = Relationship( - back_populates="incident", - sa_relationship_kwargs={"overlaps": "alerts,incidents"}, - ) + # alert_to_incident_link: List[LastAlertToIncident] = Relationship( + # back_populates="incident", + # sa_relationship_kwargs={"overlaps": "alerts,incidents"}, + # ) is_predicted: bool = Field(default=False) is_confirmed: bool = Field(default=False) @@ -183,10 +212,7 @@ class Incident(SQLModel, table=True): ), ) - def __init__(self, **kwargs): - super().__init__(**kwargs) - if "alerts" not in kwargs: - self.alerts = [] + _alerts: List["Alert"] = PrivateAttr() class Config: arbitrary_types_allowed = True @@ -224,24 +250,6 @@ class Alert(SQLModel, table=True): } ) - incidents: List["Incident"] = Relationship( - back_populates="alerts", - link_model=AlertToIncident, - sa_relationship_kwargs={ - # primaryjoin is used to filter out deleted links for various DB dialects - "primaryjoin": f"""and_(AlertToIncident.alert_id == Alert.id, - or_( - AlertToIncident.deleted_at == '{NULL_FOR_DELETED_AT.strftime('%Y-%m-%d %H:%M:%S.%f')}', - AlertToIncident.deleted_at == '{NULL_FOR_DELETED_AT.strftime('%Y-%m-%d %H:%M:%S')}' - ))""", - "uselist": True, - "overlaps": "alert,incident", - }, - ) - alert_to_incident_link: List[AlertToIncident] = Relationship( - back_populates="alert", sa_relationship_kwargs={"overlaps": "alerts,incidents"} - ) - __table_args__ = ( Index( "ix_alert_tenant_fingerprint_timestamp", diff --git a/keep/api/models/db/migrations/versions/2024-11-05-22-48_bdae8684d0b4.py b/keep/api/models/db/migrations/versions/2024-11-05-22-48_bdae8684d0b4.py new file mode 100644 index 000000000..30d63bd3c --- /dev/null +++ b/keep/api/models/db/migrations/versions/2024-11-05-22-48_bdae8684d0b4.py @@ -0,0 +1,180 @@ +"""add lastalert and lastalerttoincident table + +Revision ID: bdae8684d0b4 +Revises: ef0b5b0df41c +Create Date: 2024-11-05 22:48:04.733192 + +""" +import warnings + +import sqlalchemy as sa +import sqlalchemy_utils +import sqlmodel +from alembic import op +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import Session +from sqlalchemy.sql import expression +from sqlalchemy import exc as sa_exc + +# revision identifiers, used by Alembic. +revision = "bdae8684d0b4" +down_revision = "ef0b5b0df41c" +branch_labels = None +depends_on = None + +migration_metadata = sa.MetaData() +# +# alert_to_incident_table = sa.Table( +# 'alerttoincident', +# migration_metadata, +# sa.Column("tenant_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False), +# sa.Column('alert_id', UUID(as_uuid=False), sa.ForeignKey('alert.id', ondelete='CASCADE'), primary_key=True), +# sa.Column('incident_id', UUID(as_uuid=False), sa.ForeignKey('incident.id', ondelete='CASCADE'), primary_key=True), +# sa.Column("timestamp", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()), +# sa.Column("is_created_by_ai", sa.Boolean(), nullable=False, server_default=expression.false()), +# sa.Column("deleted_at", sa.DateTime(), nullable=False, server_default="1000-01-01 00:00:00"), +# +# ) +# +# # The following code will shoow SA warning about dialect, so we suppress it. +# with warnings.catch_warnings(): +# warnings.simplefilter("ignore", category=sa_exc.SAWarning) +# incident_table = sa.Table( +# 'incident', +# migration_metadata, +# sa.Column('id', UUID(as_uuid=False), primary_key=True), +# sa.Column('alerts_count', sa.Integer, default=0), +# sa.Column('affected_services', sa.JSON, default_factory=list), +# sa.Column('sources', sa.JSON, default_factory=list) +# ) +# +# alert_table = sa.Table( +# 'alert', +# migration_metadata, +# sa.Column('id', UUID(as_uuid=False), primary_key=True), +# sa.Column('fingerprint', sa.String), +# sa.Column('provider_type', sa.String), +# sa.Column('event', sa.JSON) +# ) + +# +def populate_db(): + session = Session(op.get_bind()) + + if session.bind.dialect.name == "postgresql": + migrate_lastalert_query = """ + insert into lastalert (fingerprint, alert_id, timestamp) + select alert.fingerprint, alert.id as alert_id, alert.timestamp + from alert + join ( + select + alert.fingerprint, max(alert.timestamp) as last_received + from alert + group by fingerprint + ) as a ON alert.fingerprint = a.fingerprint and alert.timestamp = a.last_received + on conflict + do nothing + """ + + migrate_lastalerttoincodent_query = """ + insert into lastalerttoincident (incident_id, tenant_id, timestamp, fingerprint, is_created_by_ai, deleted_at) + select ati.incident_id, ati.tenant_id, ati.timestamp, lf.fingerprint, ati.is_created_by_ai, ati.deleted_at + from alerttoincident as ati + join + ( + select alert.id, alert.fingerprint + from alert + join ( + select + alert.fingerprint, max(alert.timestamp) as last_received + from alert + group by fingerprint + ) as a on alert.fingerprint = a.fingerprint and alert.timestamp = a.last_received + ) as lf on ati.alert_id = lf.id + on conflict + do nothing + """ + + else: + migrate_lastalert_query = """ + replace into lastalert (fingerprint, alert_id, timestamp) + select alert.fingerprint, alert.id as alert_id, alert.timestamp + from alert + join ( + select + alert.fingerprint, max(alert.timestamp) as last_received + from alert + group by fingerprint + ) as a ON alert.fingerprint = a.fingerprint and alert.timestamp = a.last_received; + """ + + migrate_lastalerttoincodent_query = """ + replace into lastalerttoincident (incident_id, tenant_id, timestamp, fingerprint, is_created_by_ai, deleted_at) + select ati.incident_id, ati.tenant_id, ati.timestamp, lf.fingerprint, ati.is_created_by_ai, ati.deleted_at + from alerttoincident as ati + join + ( + select alert.id, alert.fingerprint + from alert + join ( + select + alert.fingerprint, max(alert.timestamp) as last_received + from alert + group by fingerprint + ) as a on alert.fingerprint = a.fingerprint and alert.timestamp = a.last_received + ) as lf on ati.alert_id = lf.id + """ + + session.execute(migrate_lastalert_query) + session.execute(migrate_lastalerttoincodent_query) + + +def upgrade() -> None: + op.create_table( + "lastalert", + sa.Column("fingerprint", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("alert_id", sqlmodel.sql.sqltypes.GUID(), nullable=False), + sa.Column("timestamp", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint( + ["alert_id"], + ["alert.id"], + ), + sa.PrimaryKeyConstraint("fingerprint"), + ) + with op.batch_alter_table("lastalert", schema=None) as batch_op: + batch_op.create_index( + batch_op.f("ix_lastalert_timestamp"), ["timestamp"], unique=False + ) + + op.create_table( + "lastalerttoincident", + sa.Column( + "incident_id", + sqlalchemy_utils.types.uuid.UUIDType(binary=False), + nullable=False, + ), + sa.Column("tenant_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("timestamp", sa.DateTime(), nullable=False), + sa.Column("fingerprint", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("is_created_by_ai", sa.Boolean(), nullable=False), + sa.Column("deleted_at", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint( + ["fingerprint"], + ["lastalert.fingerprint"], + ), + sa.ForeignKeyConstraint(["incident_id"], ["incident.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["tenant_id"], + ["tenant.id"], + ), + sa.PrimaryKeyConstraint("incident_id", "fingerprint", "deleted_at"), + ) + + populate_db() + +def downgrade() -> None: + op.drop_table("lastalerttoincident") + with op.batch_alter_table("lastalert", schema=None) as batch_op: + batch_op.drop_index(batch_op.f("ix_lastalert_timestamp")) + + op.drop_table("lastalert") diff --git a/keep/api/routes/incidents.py b/keep/api/routes/incidents.py index 4820a415a..6ff252c67 100644 --- a/keep/api/routes/incidents.py +++ b/keep/api/routes/incidents.py @@ -611,7 +611,7 @@ def change_incident_status( # TODO: same this change to audit table with the comment if change.status == IncidentStatus.RESOLVED: - for alert in incident.alerts: + for alert in incident._alerts: _enrich_alert( EnrichAlertRequestBody( enrichments={"status": "resolved"}, diff --git a/keep/api/tasks/process_event_task.py b/keep/api/tasks/process_event_task.py index 5a5aae8b1..330d42397 100644 --- a/keep/api/tasks/process_event_task.py +++ b/keep/api/tasks/process_event_task.py @@ -23,6 +23,7 @@ get_all_presets_dtos, get_enrichment_with_session, get_session_sync, + set_last_alert, ) from keep.api.core.dependencies import get_pusher_client from keep.api.core.elastic import ElasticClient @@ -188,6 +189,9 @@ def __save_to_db( ) session.add(audit) alert_dto = AlertDto(**formatted_event.dict()) + + set_last_alert(tenant_id, alert, session=session) + # Mapping try: enrichments_bl.run_mapping_rules(alert_dto) @@ -406,7 +410,7 @@ def __handle_formatted_events( # logger.info("Adding group alerts to the workflow manager queue") # workflow_manager.insert_events(tenant_id, grouped_alerts) # logger.info("Added group alerts to the workflow manager queue") - except Exception: + except Exception as ex: logger.exception( "Failed to run rules engine", extra={ diff --git a/keep/api/utils/enrichment_helpers.py b/keep/api/utils/enrichment_helpers.py index 86e9795be..c3af0321a 100644 --- a/keep/api/utils/enrichment_helpers.py +++ b/keep/api/utils/enrichment_helpers.py @@ -1,8 +1,12 @@ import logging from datetime import datetime +from optparse import Option +from typing import Optional from opentelemetry import trace +from sqlmodel import Session +from keep.api.core.db import existed_or_new_session from keep.api.models.alert import AlertDto, AlertStatus, AlertWithIncidentLinkMetadataDto from keep.api.models.db.alert import Alert, AlertToIncident @@ -78,7 +82,8 @@ def calculated_start_firing_time( def convert_db_alerts_to_dto_alerts( alerts: list[Alert | tuple[Alert, AlertToIncident]], - with_incidents: bool = False + with_incidents: bool = False, + session: Optional[Session] = None, ) -> list[AlertDto | AlertWithIncidentLinkMetadataDto]: """ Enriches the alerts with the enrichment data. @@ -90,46 +95,47 @@ def convert_db_alerts_to_dto_alerts( Returns: list[AlertDto | AlertWithIncidentLinkMetadataDto]: The enriched alerts. """ - alerts_dto = [] - with tracer.start_as_current_span("alerts_enrichment"): - # enrich the alerts with the enrichment data - for _object in alerts: - - # We may have an Alert only or and Alert with an AlertToIncident - if isinstance(_object, Alert): - alert, alert_to_incident = _object, None - else: - alert, alert_to_incident = _object - - if alert.alert_enrichment: - alert.event.update(alert.alert_enrichment.enrichments) - if with_incidents: - if alert.incidents: - alert.event["incident"] = ",".join(str(incident.id) for incident in alert.incidents) - try: - if alert_to_incident is not None: - alert_dto = AlertWithIncidentLinkMetadataDto.from_db_instance(alert, alert_to_incident) + with existed_or_new_session(session) as session: + alerts_dto = [] + with tracer.start_as_current_span("alerts_enrichment"): + # enrich the alerts with the enrichment data + for _object in alerts: + + # We may have an Alert only or and Alert with an AlertToIncident + if isinstance(_object, Alert): + alert, alert_to_incident = _object, None else: - alert_dto = AlertDto(**alert.event) + alert, alert_to_incident = _object + if alert.alert_enrichment: - parse_and_enrich_deleted_and_assignees( - alert_dto, alert.alert_enrichment.enrichments + alert.event.update(alert.alert_enrichment.enrichments) + if with_incidents: + if alert.incidents: + alert.event["incident"] = ",".join(str(incident.id) for incident in alert.incidents) + try: + if alert_to_incident is not None: + alert_dto = AlertWithIncidentLinkMetadataDto.from_db_instance(alert, alert_to_incident) + else: + alert_dto = AlertDto(**alert.event) + if alert.alert_enrichment: + parse_and_enrich_deleted_and_assignees( + alert_dto, alert.alert_enrichment.enrichments + ) + except Exception: + # should never happen but just in case + logger.exception( + "Failed to parse alert", + extra={ + "alert": alert, + }, ) - except Exception: - # should never happen but just in case - logger.exception( - "Failed to parse alert", - extra={ - "alert": alert, - }, - ) - continue - - alert_dto.event_id = str(alert.id) - - # enrich provider id when it's possible - if alert_dto.providerId is None: - alert_dto.providerId = alert.provider_id - alert_dto.providerType = alert.provider_type - alerts_dto.append(alert_dto) + continue + + alert_dto.event_id = str(alert.id) + + # enrich provider id when it's possible + if alert_dto.providerId is None: + alert_dto.providerId = alert.provider_id + alert_dto.providerType = alert.provider_type + alerts_dto.append(alert_dto) return alerts_dto diff --git a/keep/rulesengine/rulesengine.py b/keep/rulesengine/rulesengine.py index 363901c16..03538b8e3 100644 --- a/keep/rulesengine/rulesengine.py +++ b/keep/rulesengine/rulesengine.py @@ -81,7 +81,7 @@ def run_rules( ) incident = assign_alert_to_incident( - alert_id=event.event_id, + fingerprint=event.fingerprint, incident=incident, tenant_id=self.tenant_id, session=session, @@ -101,7 +101,7 @@ def run_rules( ): should_resolve = True - if ( + elif ( rule.resolve_on == ResolveOn.LAST.value and is_last_incident_alert_resolved(incident, session=session) ): diff --git a/tests/conftest.py b/tests/conftest.py index 06725ca5b..f73b386c6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,6 +16,7 @@ from sqlmodel import SQLModel, Session, create_engine from starlette_context import context, request_cycle_context +from keep.api.core.db import set_last_alert # This import is required to create the tables from keep.api.core.dependencies import SINGLE_TENANT_UUID from keep.api.core.elastic import ElasticClient @@ -547,6 +548,10 @@ def _setup_stress_alerts_no_elastic(num_alerts): db_session.add_all(alerts) db_session.commit() + last_alerts = [] + for alert in alerts: + set_last_alert(SINGLE_TENANT_UUID, alert, db_session) + return alerts return _setup_stress_alerts_no_elastic diff --git a/tests/test_incidents.py b/tests/test_incidents.py index e675c9075..cc1e32a90 100644 --- a/tests/test_incidents.py +++ b/tests/test_incidents.py @@ -2,8 +2,7 @@ from itertools import cycle import pytest -from sqlalchemy import distinct, func -from sqlalchemy.orm.exc import DetachedInstanceError +from sqlalchemy import distinct, func, desc from keep.api.core.db import ( IncidentSorting, @@ -14,7 +13,7 @@ get_incident_by_id, get_last_incidents, merge_incidents_to_id, - remove_alerts_to_incident_by_incident_id, + remove_alerts_to_incident_by_incident_id, enrich_incidents_with_alerts, ) from keep.api.core.db_utils import get_json_extract_field from keep.api.core.dependencies import SINGLE_TENANT_UUID @@ -24,7 +23,7 @@ IncidentSeverity, IncidentStatus, ) -from keep.api.models.db.alert import Alert, AlertToIncident +from keep.api.models.db.alert import Alert, AlertToIncident, LastAlertToIncident from keep.api.utils.enrichment_helpers import convert_db_alerts_to_dto_alerts from tests.fixtures.client import client, test_app # noqa @@ -50,7 +49,7 @@ def test_get_alerts_data_for_incident(db_session, create_alert): assert 100 == db_session.query(func.count(Alert.id)).scalar() assert 10 == unique_fingerprints - data = get_alerts_data_for_incident(SINGLE_TENANT_UUID, [a.id for a in alerts]) + data = get_alerts_data_for_incident(SINGLE_TENANT_UUID, [a.fingerprint for a in alerts]) assert data["sources"] == set([f"source_{i}" for i in range(10)]) assert data["services"] == set([f"service_{i}" for i in range(10)]) assert data["count"] == unique_fingerprints @@ -64,16 +63,27 @@ def test_add_remove_alert_to_incidents(db_session, setup_stress_alerts_no_elasti SINGLE_TENANT_UUID, {"user_generated_name": "test", "user_summary": "test"} ) - assert len(incident.alerts) == 0 + incident_alerts, total_incident_alerts = get_incident_alerts_by_incident_id( + tenant_id=SINGLE_TENANT_UUID, + incident_id=incident.id, + ) + + assert len(incident_alerts) == 0 + assert total_incident_alerts == 0 add_alerts_to_incident_by_incident_id( - SINGLE_TENANT_UUID, incident.id, [a.id for a in alerts] + SINGLE_TENANT_UUID, incident.id, [a.fingerprint for a in alerts] ) incident = get_incident_by_id(SINGLE_TENANT_UUID, incident.id) - # 110 alerts - assert len(incident.alerts) == 110 + incident_alerts, total_incident_alerts = get_incident_alerts_by_incident_id( + tenant_id=SINGLE_TENANT_UUID, + incident_id=incident.id, + ) + + assert len(incident_alerts) == 100 + assert total_incident_alerts == 100 # But 100 unique fingerprints assert incident.alerts_count == 100 @@ -86,7 +96,7 @@ def test_add_remove_alert_to_incidents(db_session, setup_stress_alerts_no_elasti service_field = get_json_extract_field(db_session, Alert.event, "service") - service_0 = db_session.query(Alert.id).filter(service_field == "service_0").all() + service_0 = db_session.query(Alert.fingerprint).filter(service_field == "service_0").all() # Testing unique fingerprints more_alerts_with_same_fingerprints = setup_stress_alerts_no_elastic(10) @@ -94,26 +104,32 @@ def test_add_remove_alert_to_incidents(db_session, setup_stress_alerts_no_elasti add_alerts_to_incident_by_incident_id( SINGLE_TENANT_UUID, incident.id, - [a.id for a in more_alerts_with_same_fingerprints], + [a.fingerprint for a in more_alerts_with_same_fingerprints], ) incident = get_incident_by_id(SINGLE_TENANT_UUID, incident.id) assert incident.alerts_count == 100 - assert db_session.query(func.count(AlertToIncident.alert_id)).scalar() == 120 + assert db_session.query(func.count(LastAlertToIncident.fingerprint)).scalar() == 100 remove_alerts_to_incident_by_incident_id( SINGLE_TENANT_UUID, incident.id, [ - service_0[0].id, + service_0[0].fingerprint, ], ) incident = get_incident_by_id(SINGLE_TENANT_UUID, incident.id) - # 117 because we removed multiple alerts with service_0 - assert len(incident.alerts) == 117 + incident_alerts, total_incident_alerts = get_incident_alerts_by_incident_id( + tenant_id=SINGLE_TENANT_UUID, + incident_id=incident.id, + ) + + assert len(incident_alerts) == 99 + assert total_incident_alerts == 99 + assert "service_0" in incident.affected_services assert len(incident.affected_services) == 10 assert sorted(incident.affected_services) == sorted( @@ -121,7 +137,7 @@ def test_add_remove_alert_to_incidents(db_session, setup_stress_alerts_no_elasti ) remove_alerts_to_incident_by_incident_id( - SINGLE_TENANT_UUID, incident.id, [a.id for a in service_0] + SINGLE_TENANT_UUID, incident.id, [a.fingerprint for a in service_0] ) # Removing shouldn't impact links between alert and incident if include_unlinked=True @@ -138,8 +154,14 @@ def test_add_remove_alert_to_incidents(db_session, setup_stress_alerts_no_elasti incident = get_incident_by_id(SINGLE_TENANT_UUID, incident.id) - # 108 because we removed multiple alert with same fingerprints - assert len(incident.alerts) == 108 + incident_alerts, total_incident_alerts = get_incident_alerts_by_incident_id( + tenant_id=SINGLE_TENANT_UUID, + incident_id=incident.id, + ) + + assert len(incident_alerts) == 90 + assert total_incident_alerts == 90 + assert "service_0" not in incident.affected_services assert len(incident.affected_services) == 9 assert sorted(incident.affected_services) == sorted( @@ -147,29 +169,36 @@ def test_add_remove_alert_to_incidents(db_session, setup_stress_alerts_no_elasti ) source_1 = ( - db_session.query(Alert.id).filter(Alert.provider_type == "source_1").all() + db_session.query(Alert.fingerprint).filter(Alert.provider_type == "source_1").all() ) remove_alerts_to_incident_by_incident_id( SINGLE_TENANT_UUID, incident.id, [ - source_1[0].id, + source_1[0].fingerprint, ], ) incident = get_incident_by_id(SINGLE_TENANT_UUID, incident.id) - assert len(incident.alerts) == 105 - assert "source_1" in incident.sources - # source_0 was removed together with service_0 - assert len(incident.sources) == 9 + incident_alerts, total_incident_alerts = get_incident_alerts_by_incident_id( + tenant_id=SINGLE_TENANT_UUID, + incident_id=incident.id, + ) + + assert len(incident_alerts) == 89 + assert total_incident_alerts == 89 + + assert "source_1" not in incident.sources + # source_0 was removed together with service_1 + assert len(incident.sources) == 8 assert sorted(incident.sources) == sorted( - ["source_{}".format(i) for i in range(1, 10)] + ["source_{}".format(i) for i in range(2, 10)] ) remove_alerts_to_incident_by_incident_id( - "keep", incident.id, [a.id for a in source_1] + "keep", incident.id, [a.fingerprint for a in source_1] ) incident = get_incident_by_id(SINGLE_TENANT_UUID, incident.id) @@ -213,8 +242,19 @@ def test_get_last_incidents(db_session, create_alert): ) alert = db_session.query(Alert).order_by(Alert.timestamp.desc()).first() + create_alert( + f"alert-test-2-{i}", + AlertStatus(status), + datetime.utcnow(), + { + "severity": AlertSeverity.from_number(severity), + "service": service, + }, + ) + alert2 = db_session.query(Alert).order_by(Alert.timestamp.desc()).first() + add_alerts_to_incident_by_incident_id( - SINGLE_TENANT_UUID, incident.id, [alert.id] + SINGLE_TENANT_UUID, incident.id, [alert.fingerprint, alert2.fingerprint] ) incidents_default, incidents_default_count = get_last_incidents(SINGLE_TENANT_UUID) @@ -246,19 +286,14 @@ def test_get_last_incidents(db_session, create_alert): for i, j in enumerate(range(5, 10)): assert incidents_limit_5_page_2[i].user_generated_name == f"test-{j}" - # If alerts not preloaded, we will have detached session issue during attempt to get them - # Background on this error at: https://sqlalche.me/e/14/bhk3 - with pytest.raises(DetachedInstanceError): - alerts = incidents_confirmed[0].alerts # noqa - incidents_with_alerts, _ = get_last_incidents( SINGLE_TENANT_UUID, is_confirmed=True, with_alerts=True ) for i in range(25): if incidents_with_alerts[i].status == IncidentStatus.MERGED.value: - assert len(incidents_with_alerts[i].alerts) == 0 + assert len(incidents_with_alerts[i]._alerts) == 0 else: - assert len(incidents_with_alerts[i].alerts) == 1 + assert len(incidents_with_alerts[i]._alerts) == 2 # Test sorting @@ -319,11 +354,16 @@ def test_incident_status_change( "keep", {"name": "test", "description": "test"} ) - add_alerts_to_incident_by_incident_id("keep", incident.id, [a.id for a in alerts]) + add_alerts_to_incident_by_incident_id( + "keep", + incident.id, + [a.fingerprint for a in alerts], + session=db_session + ) - incident = get_incident_by_id("keep", incident.id, with_alerts=True) + incident = get_incident_by_id("keep", incident.id, with_alerts=True, session=db_session) - alerts_dtos = convert_db_alerts_to_dto_alerts(incident.alerts) + alerts_dtos = convert_db_alerts_to_dto_alerts(incident._alerts, session=db_session) assert ( len( [ @@ -348,10 +388,11 @@ def test_incident_status_change( assert data["id"] == str(incident.id) assert data["status"] == IncidentStatus.ACKNOWLEDGED.value - incident = get_incident_by_id("keep", incident.id, with_alerts=True) + db_session.expire_all() + incident = get_incident_by_id("keep", incident.id, with_alerts=True, session=db_session) assert incident.status == IncidentStatus.ACKNOWLEDGED.value - alerts_dtos = convert_db_alerts_to_dto_alerts(incident.alerts) + alerts_dtos = convert_db_alerts_to_dto_alerts(incident._alerts) assert ( len( [ @@ -376,11 +417,12 @@ def test_incident_status_change( assert data["id"] == str(incident.id) assert data["status"] == IncidentStatus.RESOLVED.value - incident = get_incident_by_id("keep", incident.id, with_alerts=True) + db_session.expire_all() + incident = get_incident_by_id("keep", incident.id, with_alerts=True, session=db_session) assert incident.status == IncidentStatus.RESOLVED.value # All alerts are resolved as well - alerts_dtos = convert_db_alerts_to_dto_alerts(incident.alerts) + alerts_dtos = convert_db_alerts_to_dto_alerts(incident._alerts, session=db_session) assert ( len( [ @@ -475,23 +517,48 @@ def test_add_alerts_with_same_fingerprint_to_incident(db_session, create_alert): SINGLE_TENANT_UUID, {"user_generated_name": "test", "user_summary": "test"} ) - assert len(incident.alerts) == 0 + incident_alerts, total_incident_alerts = get_incident_alerts_by_incident_id( + tenant_id=SINGLE_TENANT_UUID, + incident_id=incident.id, + ) + + assert len(incident_alerts) == 0 + assert total_incident_alerts == 0 add_alerts_to_incident_by_incident_id( - SINGLE_TENANT_UUID, incident.id, [fp1_alerts[0].id] + SINGLE_TENANT_UUID, incident.id, [fp1_alerts[0].fingerprint] ) incident = get_incident_by_id(SINGLE_TENANT_UUID, incident.id) - assert len(incident.alerts) == 2 + incident_alerts, total_incident_alerts = get_incident_alerts_by_incident_id( + tenant_id=SINGLE_TENANT_UUID, + incident_id=incident.id, + ) + + assert len(incident_alerts) == 1 + last_fp1_alert = ( + db_session + .query(Alert.timestamp) + .where(Alert.fingerprint == "fp1") + .order_by(desc(Alert.timestamp)).first() + ) + assert incident_alerts[0].timestamp == last_fp1_alert.timestamp + assert total_incident_alerts == 1 remove_alerts_to_incident_by_incident_id( - SINGLE_TENANT_UUID, incident.id, [fp1_alerts[0].id] + SINGLE_TENANT_UUID, incident.id, [fp1_alerts[0].fingerprint] ) incident = get_incident_by_id(SINGLE_TENANT_UUID, incident.id) - assert len(incident.alerts) == 0 + incident_alerts, total_incident_alerts = get_incident_alerts_by_incident_id( + tenant_id=SINGLE_TENANT_UUID, + incident_id=incident.id, + ) + + assert len(incident_alerts) == 0 + assert total_incident_alerts == 0 def test_merge_incidents(db_session, create_alert, setup_stress_alerts_no_elastic): @@ -522,7 +589,7 @@ def test_merge_incidents(db_session, create_alert, setup_stress_alerts_no_elasti ) alerts_1 = db_session.query(Alert).all() add_alerts_to_incident_by_incident_id( - SINGLE_TENANT_UUID, incident_1.id, [a.id for a in alerts_1] + SINGLE_TENANT_UUID, incident_1.id, [a.fingerprint for a in alerts_1] ) incident_2 = create_incident_from_dict( SINGLE_TENANT_UUID, @@ -532,19 +599,19 @@ def test_merge_incidents(db_session, create_alert, setup_stress_alerts_no_elasti }, ) create_alert( - "fp20", + "fp20-0", AlertStatus.FIRING, datetime.utcnow(), {"severity": AlertSeverity.CRITICAL.value}, ) create_alert( - "fp20", + "fp20-1", AlertStatus.FIRING, datetime.utcnow(), {"severity": AlertSeverity.CRITICAL.value}, ) create_alert( - "fp20", + "fp20-2", AlertStatus.FIRING, datetime.utcnow(), {"severity": AlertSeverity.CRITICAL.value}, @@ -553,7 +620,7 @@ def test_merge_incidents(db_session, create_alert, setup_stress_alerts_no_elasti db_session.query(Alert).filter(Alert.fingerprint.startswith("fp20")).all() ) add_alerts_to_incident_by_incident_id( - SINGLE_TENANT_UUID, incident_2.id, [a.id for a in alerts_2] + SINGLE_TENANT_UUID, incident_2.id, [a.fingerprint for a in alerts_2] ) incident_3 = create_incident_from_dict( SINGLE_TENANT_UUID, @@ -563,28 +630,28 @@ def test_merge_incidents(db_session, create_alert, setup_stress_alerts_no_elasti }, ) create_alert( - "fp30", + "fp30-0", AlertStatus.FIRING, datetime.utcnow(), {"severity": AlertSeverity.WARNING.value}, ) create_alert( - "fp30", + "fp30-1", AlertStatus.FIRING, datetime.utcnow(), - {"severity": AlertSeverity.WARNING.value}, + {"severity": AlertSeverity.INFO.value}, ) create_alert( - "fp30", + "fp30-2", AlertStatus.FIRING, datetime.utcnow(), - {"severity": AlertSeverity.INFO.value}, + {"severity": AlertSeverity.WARNING.value}, ) alerts_3 = ( db_session.query(Alert).filter(Alert.fingerprint.startswith("fp30")).all() ) add_alerts_to_incident_by_incident_id( - SINGLE_TENANT_UUID, incident_3.id, [a.id for a in alerts_3] + SINGLE_TENANT_UUID, incident_3.id, [a.fingerprint for a in alerts_3] ) # before merge @@ -602,19 +669,21 @@ def test_merge_incidents(db_session, create_alert, setup_stress_alerts_no_elasti "test-user-email", ) + db_session.expire_all() + incident_1 = get_incident_by_id(SINGLE_TENANT_UUID, incident_1.id, with_alerts=True) - assert len(incident_1.alerts) == 9 + assert len(incident_1._alerts) == 8 assert incident_1.severity == IncidentSeverity.CRITICAL.order incident_2 = get_incident_by_id(SINGLE_TENANT_UUID, incident_2.id, with_alerts=True) - assert len(incident_2.alerts) == 0 + assert len(incident_2._alerts) == 0 assert incident_2.status == IncidentStatus.MERGED.value assert incident_2.merged_into_incident_id == incident_1.id assert incident_2.merged_at is not None assert incident_2.merged_by == "test-user-email" - incident_3 = get_incident_by_id(SINGLE_TENANT_UUID, incident_3.id, with_alerts=True) - assert len(incident_3.alerts) == 0 + incident_3 = get_incident_by_id(SINGLE_TENANT_UUID, incident_3.id, with_alerts=True, session=db_session) + assert len(incident_3._alerts) == 0 assert incident_3.status == IncidentStatus.MERGED.value assert incident_3.merged_into_incident_id == incident_1.id assert incident_3.merged_at is not None diff --git a/tests/test_metrics.py b/tests/test_metrics.py index fcd4306d6..8908896b6 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -18,7 +18,7 @@ def test_add_remove_alert_to_incidents( valid_api_key = "valid_api_key" setup_api_key(db_session, valid_api_key) - add_alerts_to_incident_by_incident_id("keep", incident.id, [a.id for a in alerts]) + add_alerts_to_incident_by_incident_id("keep", incident.id, [a.fingerprint for a in alerts]) response = client.get("/metrics?labels=a.b", headers={"X-API-KEY": "valid_api_key"}) diff --git a/tests/test_rules_engine.py b/tests/test_rules_engine.py index 40627b372..07ad6cf70 100644 --- a/tests/test_rules_engine.py +++ b/tests/test_rules_engine.py @@ -7,7 +7,7 @@ import pytest from keep.api.core.db import create_rule as create_rule_db -from keep.api.core.db import get_incident_alerts_by_incident_id, get_last_incidents +from keep.api.core.db import get_incident_alerts_by_incident_id, get_last_incidents, set_last_alert from keep.api.core.db import get_rules as get_rules_db from keep.api.core.dependencies import SINGLE_TENANT_UUID from keep.api.models.alert import ( @@ -63,10 +63,13 @@ def test_sanity(db_session): provider_type="test", provider_id="test", event=alerts[0].dict(), - fingerprint="test", + fingerprint=alerts[0].fingerprint, ) + db_session.add(alert) db_session.commit() + + set_last_alert(SINGLE_TENANT_UUID, alert, db_session) # run the rules engine alerts[0].event_id = alert.id results = rules_engine.run_rules(alerts) @@ -110,10 +113,11 @@ def test_sanity_2(db_session): provider_type="test", provider_id="test", event=alerts[0].dict(), - fingerprint="test", + fingerprint=alerts[0].fingerprint, ) db_session.add(alert) db_session.commit() + set_last_alert(SINGLE_TENANT_UUID, alert, db_session) # run the rules engine alerts[0].event_id = alert.id results = rules_engine.run_rules(alerts) @@ -158,10 +162,11 @@ def test_sanity_3(db_session): provider_type="test", provider_id="test", event=alerts[0].dict(), - fingerprint="test", + fingerprint=alerts[0].fingerprint, ) db_session.add(alert) db_session.commit() + set_last_alert(SINGLE_TENANT_UUID, alert, db_session) # run the rules engine alerts[0].event_id = alert.id results = rules_engine.run_rules(alerts) @@ -206,10 +211,11 @@ def test_sanity_4(db_session): provider_type="test", provider_id="test", event=alerts[0].dict(), - fingerprint="test", + fingerprint=alerts[0].fingerprint, ) db_session.add(alert) db_session.commit() + set_last_alert(SINGLE_TENANT_UUID, alert, db_session) # run the rules engine alerts[0].event_id = alert.id results = rules_engine.run_rules(alerts) @@ -223,7 +229,7 @@ def test_incident_attributes(db_session): AlertDto( id=str(uuid.uuid4()), source=["grafana"], - name="grafana-test-alert", + name=f"grafana-test-alert-{i}", status=AlertStatus.FIRING, severity=AlertSeverity.CRITICAL, lastReceived=datetime.datetime.now().isoformat(), @@ -255,13 +261,15 @@ def test_incident_attributes(db_session): provider_type="test", provider_id="test", event=alert.dict(), - fingerprint=hashlib.sha256(json.dumps(alert.dict()).encode()).hexdigest(), + fingerprint=alert.fingerprint, timestamp=alert.lastReceived, ) for alert in alerts_dto ] db_session.add_all(alerts) db_session.commit() + for alert in alerts: + set_last_alert(SINGLE_TENANT_UUID, alert, db_session) for i, alert in enumerate(alerts_dto): alert.event_id = alerts[i].id @@ -283,7 +291,7 @@ def test_incident_severity(db_session): AlertDto( id=str(uuid.uuid4()), source=["grafana"], - name="grafana-test-alert", + name=f"grafana-test-alert-{i}", status=AlertStatus.FIRING, severity=AlertSeverity.INFO, lastReceived=datetime.datetime.now().isoformat(), @@ -315,13 +323,15 @@ def test_incident_severity(db_session): provider_type="test", provider_id="test", event=alert.dict(), - fingerprint=hashlib.sha256(json.dumps(alert.dict()).encode()).hexdigest(), + fingerprint=alert.fingerprint, timestamp=alert.lastReceived, ) for alert in alerts_dto ] db_session.add_all(alerts) db_session.commit() + for alert in alerts: + set_last_alert(SINGLE_TENANT_UUID, alert, db_session) for i, alert in enumerate(alerts_dto): alert.event_id = alerts[i].id From a489553a68f64c0256b34793dabc87098476a7db Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Wed, 13 Nov 2024 16:53:52 +0400 Subject: [PATCH 02/26] Fix some uncovered issues --- .../alerts/alert-associate-incident-modal.tsx | 2 +- .../[id]/alerts/incident-alert-menu.tsx | 2 +- keep-ui/entities/incidents/model/models.ts | 1 + keep/api/bl/incidents_bl.py | 26 +++++----- keep/api/core/db.py | 1 - ...b4.py => 2024-11-13-22-48_bdae8684d0b4.py} | 47 +++---------------- keep/api/routes/incidents.py | 8 ++-- keep/api/routes/workflows.py | 3 +- 8 files changed, 29 insertions(+), 61 deletions(-) rename keep/api/models/db/migrations/versions/{2024-11-05-22-48_bdae8684d0b4.py => 2024-11-13-22-48_bdae8684d0b4.py} (73%) diff --git a/keep-ui/app/(keep)/alerts/alert-associate-incident-modal.tsx b/keep-ui/app/(keep)/alerts/alert-associate-incident-modal.tsx index e8b85f1ee..fb1d8a8bc 100644 --- a/keep-ui/app/(keep)/alerts/alert-associate-incident-modal.tsx +++ b/keep-ui/app/(keep)/alerts/alert-associate-incident-modal.tsx @@ -42,7 +42,7 @@ const AlertAssociateIncidentModal = ({ try { const response = await api.post( `/incidents/${incidentId}/alerts`, - alerts.map(({ event_id }) => event_id) + alerts.map(({ fingerprint }) => fingerprint) ); handleSuccess(); await mutate(); diff --git a/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-menu.tsx b/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-menu.tsx index fb3ab512a..5ef346b8c 100644 --- a/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-menu.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-menu.tsx @@ -18,7 +18,7 @@ export default function IncidentAlertMenu({ incidentId, alert }: Props) { if (confirm("Are you sure you want to remove correlation?")) { api .delete(`/incidents/${incidentId}/alerts`, { - body: [alert.event_id], + body: [alert.fingerprint], }) .then(() => { toast.success("Alert removed from incident successfully", { diff --git a/keep-ui/entities/incidents/model/models.ts b/keep-ui/entities/incidents/model/models.ts index 0f8e8cfb2..bdc311549 100644 --- a/keep-ui/entities/incidents/model/models.ts +++ b/keep-ui/entities/incidents/model/models.ts @@ -31,6 +31,7 @@ export interface IncidentDto { merged_into_incident_id: string; merged_by: string; merged_at: Date; + fingerprint: string; } export interface IncidentCandidateDto { diff --git a/keep/api/bl/incidents_bl.py b/keep/api/bl/incidents_bl.py index a0bd58584..5bc3d9216 100644 --- a/keep/api/bl/incidents_bl.py +++ b/keep/api/bl/incidents_bl.py @@ -93,51 +93,51 @@ def create_incident( return new_incident_dto async def add_alerts_to_incident( - self, incident_id: UUID, alert_ids: List[UUID], is_created_by_ai: bool = False + self, incident_id: UUID, alert_fingerprints: List[str], is_created_by_ai: bool = False ) -> None: self.logger.info( "Adding alerts to incident", - extra={"incident_id": incident_id, "alert_ids": alert_ids}, + extra={"incident_id": incident_id, "alert_fingerprints": alert_fingerprints}, ) incident = get_incident_by_id(tenant_id=self.tenant_id, incident_id=incident_id) if not incident: raise HTTPException(status_code=404, detail="Incident not found") - add_alerts_to_incident_by_incident_id(self.tenant_id, incident_id, alert_ids, is_created_by_ai) + add_alerts_to_incident_by_incident_id(self.tenant_id, incident_id, alert_fingerprints, is_created_by_ai) self.logger.info( "Alerts added to incident", - extra={"incident_id": incident_id, "alert_ids": alert_ids}, + extra={"incident_id": incident_id, "alert_fingerprints": alert_fingerprints}, ) - self.__update_elastic(incident_id, alert_ids) + self.__update_elastic(incident_id, alert_fingerprints) self.logger.info( "Alerts pushed to elastic", - extra={"incident_id": incident_id, "alert_ids": alert_ids}, + extra={"incident_id": incident_id, "alert_fingerprints": alert_fingerprints}, ) self.__update_client_on_incident_change(incident_id) self.logger.info( "Client updated on incident change", - extra={"incident_id": incident_id, "alert_ids": alert_ids}, + extra={"incident_id": incident_id, "alert_fingerprints": alert_fingerprints}, ) incident_dto = IncidentDto.from_db_incident(incident) self.__run_workflows(incident_dto, "updated") self.logger.info( "Workflows run on incident", - extra={"incident_id": incident_id, "alert_ids": alert_ids}, + extra={"incident_id": incident_id, "alert_fingerprints": alert_fingerprints}, ) await self.__generate_summary(incident_id, incident) self.logger.info( "Summary generated", - extra={"incident_id": incident_id, "alert_ids": alert_ids}, + extra={"incident_id": incident_id, "alert_fingerprints": alert_fingerprints}, ) - def __update_elastic(self, incident_id: UUID, alert_ids: List[UUID]): + def __update_elastic(self, incident_id: UUID, alert_fingerprints: List[str]): try: elastic_client = ElasticClient(self.tenant_id) if elastic_client.enabled: db_alerts, _ = get_incident_alerts_by_incident_id( tenant_id=self.tenant_id, incident_id=incident_id, - limit=len(alert_ids), + limit=len(alert_fingerprints), ) enriched_alerts_dto = convert_db_alerts_to_dto_alerts( db_alerts, with_incidents=True @@ -203,7 +203,7 @@ async def __generate_summary(self, incident_id: UUID, incident: Incident): ) def delete_alerts_from_incident( - self, incident_id: UUID, alert_ids: List[UUID] + self, incident_id: UUID, alert_fingerprints: List[str] ) -> None: self.logger.info( "Fetching incident", @@ -216,7 +216,7 @@ def delete_alerts_from_incident( if not incident: raise HTTPException(status_code=404, detail="Incident not found") - remove_alerts_to_incident_by_incident_id(self.tenant_id, incident_id, alert_ids) + remove_alerts_to_incident_by_incident_id(self.tenant_id, incident_id, alert_fingerprints) def delete_incident(self, incident_id: UUID) -> None: self.logger.info( diff --git a/keep/api/core/db.py b/keep/api/core/db.py index f4a9a1455..f0ec2e5e8 100644 --- a/keep/api/core/db.py +++ b/keep/api/core/db.py @@ -3302,7 +3302,6 @@ def update_incident_from_dto_by_id( Incident.tenant_id == tenant_id, Incident.id == incident_id, ) - .options(joinedload(Incident.alerts)) ).first() if not incident: diff --git a/keep/api/models/db/migrations/versions/2024-11-05-22-48_bdae8684d0b4.py b/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py similarity index 73% rename from keep/api/models/db/migrations/versions/2024-11-05-22-48_bdae8684d0b4.py rename to keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py index 30d63bd3c..dd7814ff6 100644 --- a/keep/api/models/db/migrations/versions/2024-11-05-22-48_bdae8684d0b4.py +++ b/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py @@ -18,53 +18,19 @@ # revision identifiers, used by Alembic. revision = "bdae8684d0b4" -down_revision = "ef0b5b0df41c" +down_revision = "620b6c048091" branch_labels = None depends_on = None migration_metadata = sa.MetaData() -# -# alert_to_incident_table = sa.Table( -# 'alerttoincident', -# migration_metadata, -# sa.Column("tenant_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False), -# sa.Column('alert_id', UUID(as_uuid=False), sa.ForeignKey('alert.id', ondelete='CASCADE'), primary_key=True), -# sa.Column('incident_id', UUID(as_uuid=False), sa.ForeignKey('incident.id', ondelete='CASCADE'), primary_key=True), -# sa.Column("timestamp", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()), -# sa.Column("is_created_by_ai", sa.Boolean(), nullable=False, server_default=expression.false()), -# sa.Column("deleted_at", sa.DateTime(), nullable=False, server_default="1000-01-01 00:00:00"), -# -# ) -# -# # The following code will shoow SA warning about dialect, so we suppress it. -# with warnings.catch_warnings(): -# warnings.simplefilter("ignore", category=sa_exc.SAWarning) -# incident_table = sa.Table( -# 'incident', -# migration_metadata, -# sa.Column('id', UUID(as_uuid=False), primary_key=True), -# sa.Column('alerts_count', sa.Integer, default=0), -# sa.Column('affected_services', sa.JSON, default_factory=list), -# sa.Column('sources', sa.JSON, default_factory=list) -# ) -# -# alert_table = sa.Table( -# 'alert', -# migration_metadata, -# sa.Column('id', UUID(as_uuid=False), primary_key=True), -# sa.Column('fingerprint', sa.String), -# sa.Column('provider_type', sa.String), -# sa.Column('event', sa.JSON) -# ) - -# + def populate_db(): session = Session(op.get_bind()) if session.bind.dialect.name == "postgresql": migrate_lastalert_query = """ - insert into lastalert (fingerprint, alert_id, timestamp) - select alert.fingerprint, alert.id as alert_id, alert.timestamp + insert into lastalert (tenant_id, fingerprint, alert_id, timestamp) + select alert.tenant_id, alert.fingerprint, alert.id as alert_id, alert.timestamp from alert join ( select @@ -97,8 +63,8 @@ def populate_db(): else: migrate_lastalert_query = """ - replace into lastalert (fingerprint, alert_id, timestamp) - select alert.fingerprint, alert.id as alert_id, alert.timestamp + replace into lastalert (tenant_id, fingerprint, alert_id, timestamp) + select alert.tenant_id, alert.fingerprint, alert.id as alert_id, alert.timestamp from alert join ( select @@ -132,6 +98,7 @@ def populate_db(): def upgrade() -> None: op.create_table( "lastalert", + sa.Column("tenant_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False), sa.Column("fingerprint", sqlmodel.sql.sqltypes.AutoString(), nullable=False), sa.Column("alert_id", sqlmodel.sql.sqltypes.GUID(), nullable=False), sa.Column("timestamp", sa.DateTime(), nullable=False), diff --git a/keep/api/routes/incidents.py b/keep/api/routes/incidents.py index 6ff252c67..d7e4bbd39 100644 --- a/keep/api/routes/incidents.py +++ b/keep/api/routes/incidents.py @@ -457,7 +457,7 @@ def get_incident_workflows( ) async def add_alerts_to_incident( incident_id: UUID, - alert_ids: List[UUID], + alert_fingerprints: List[str], is_created_by_ai: bool = False, authenticated_entity: AuthenticatedEntity = Depends( IdentityManagerFactory.get_auth_verifier(["write:incident"]) @@ -467,7 +467,7 @@ async def add_alerts_to_incident( ): tenant_id = authenticated_entity.tenant_id incident_bl = IncidentBl(tenant_id, session, pusher_client) - await incident_bl.add_alerts_to_incident(incident_id, alert_ids, is_created_by_ai) + await incident_bl.add_alerts_to_incident(incident_id, alert_fingerprints, is_created_by_ai) return Response(status_code=202) @@ -479,7 +479,7 @@ async def add_alerts_to_incident( ) def delete_alerts_from_incident( incident_id: UUID, - alert_ids: List[UUID], + fingerprints: List[str], authenticated_entity: AuthenticatedEntity = Depends( IdentityManagerFactory.get_auth_verifier(["write:incident"]) ), @@ -489,7 +489,7 @@ def delete_alerts_from_incident( tenant_id = authenticated_entity.tenant_id incident_bl = IncidentBl(tenant_id, session, pusher_client) incident_bl.delete_alerts_from_incident( - incident_id=incident_id, alert_ids=alert_ids + incident_id=incident_id, alert_fingerprints=fingerprints ) return Response(status_code=202) diff --git a/keep/api/routes/workflows.py b/keep/api/routes/workflows.py index 8d355d08f..57f7422e7 100644 --- a/keep/api/routes/workflows.py +++ b/keep/api/routes/workflows.py @@ -190,7 +190,8 @@ def run_workflow( event_body = body.get("body", {}) or body # if its event that was triggered by the UI with the Modal - if "test-workflow" in event_body.get("fingerprint", "") or not body: + fingerprint = event_body.get("fingerprint", "") + if fingerprint and "test-workflow" in fingerprint or not body: # some random event_body["id"] = event_body.get("fingerprint", "manual-run") event_body["name"] = event_body.get("fingerprint", "manual-run") From 90959d8d92b6a1f1b5a71eaf5c8fc02cf4830494 Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Wed, 13 Nov 2024 17:05:54 +0400 Subject: [PATCH 03/26] Remove unused imports and lost usages of AlertToIncident --- keep/api/core/db.py | 75 +++++++++---------- keep/api/models/db/alert.py | 2 +- .../versions/2024-11-13-22-48_bdae8684d0b4.py | 4 - keep/api/tasks/process_event_task.py | 2 +- keep/api/utils/enrichment_helpers.py | 7 +- 5 files changed, 42 insertions(+), 48 deletions(-) diff --git a/keep/api/core/db.py b/keep/api/core/db.py index f0ec2e5e8..fa53cbdf5 100644 --- a/keep/api/core/db.py +++ b/keep/api/core/db.py @@ -1303,13 +1303,12 @@ def get_last_alerts( # SQLite version - using JSON incidents_subquery = ( session.query( - AlertToIncident.alert_id, + LastAlertToIncident.fingerprint, func.json_group_array( - cast(AlertToIncident.incident_id, String) + cast(LastAlertToIncident.incident_id, String) ).label("incidents"), ) - .filter(AlertToIncident.deleted_at == NULL_FOR_DELETED_AT) - .group_by(AlertToIncident.alert_id) + .filter(LastAlertToIncident.deleted_at == NULL_FOR_DELETED_AT) .subquery() ) @@ -1317,13 +1316,12 @@ def get_last_alerts( # MySQL version - using GROUP_CONCAT incidents_subquery = ( session.query( - AlertToIncident.alert_id, + LastAlertToIncident.fingerprint, func.group_concat( - cast(AlertToIncident.incident_id, String) + cast(LastAlertToIncident.incident_id, String) ).label("incidents"), ) - .filter(AlertToIncident.deleted_at == NULL_FOR_DELETED_AT) - .group_by(AlertToIncident.alert_id) + .filter(LastAlertToIncident.deleted_at == NULL_FOR_DELETED_AT) .subquery() ) @@ -1331,14 +1329,13 @@ def get_last_alerts( # PostgreSQL version - using string_agg incidents_subquery = ( session.query( - AlertToIncident.alert_id, + LastAlertToIncident.fingerprint, func.string_agg( - cast(AlertToIncident.incident_id, String), + cast(LastAlertToIncident.incident_id, String), ",", ).label("incidents"), ) - .filter(AlertToIncident.deleted_at == NULL_FOR_DELETED_AT) - .group_by(AlertToIncident.alert_id) + .filter(LastAlertToIncident.deleted_at == NULL_FOR_DELETED_AT) .subquery() ) else: @@ -1794,13 +1791,13 @@ def get_rule_distribution(tenant_id, minute=False): # Check the dialect if session.bind.dialect.name == "mysql": time_format = "%Y-%m-%d %H:%i" if minute else "%Y-%m-%d %H" - timestamp_format = func.date_format(AlertToIncident.timestamp, time_format) + timestamp_format = func.date_format(LastAlertToIncident.timestamp, time_format) elif session.bind.dialect.name == "postgresql": time_format = "YYYY-MM-DD HH:MI" if minute else "YYYY-MM-DD HH" - timestamp_format = func.to_char(AlertToIncident.timestamp, time_format) + timestamp_format = func.to_char(LastAlertToIncident.timestamp, time_format) elif session.bind.dialect.name == "sqlite": time_format = "%Y-%m-%d %H:%M" if minute else "%Y-%m-%d %H" - timestamp_format = func.strftime(time_format, AlertToIncident.timestamp) + timestamp_format = func.strftime(time_format, LastAlertToIncident.timestamp) else: raise ValueError("Unsupported database dialect") # Construct the query @@ -1811,13 +1808,13 @@ def get_rule_distribution(tenant_id, minute=False): Incident.id.label("group_id"), Incident.rule_fingerprint.label("rule_fingerprint"), timestamp_format.label("time"), - func.count(AlertToIncident.alert_id).label("hits"), + func.count(LastAlertToIncident.fingerprint).label("hits"), ) .join(Incident, Rule.id == Incident.rule_id) - .join(AlertToIncident, Incident.id == AlertToIncident.incident_id) + .join(LastAlertToIncident, Incident.id == LastAlertToIncident.incident_id) .filter( - AlertToIncident.deleted_at == NULL_FOR_DELETED_AT, - AlertToIncident.timestamp >= seven_days_ago, + LastAlertToIncident.deleted_at == NULL_FOR_DELETED_AT, + LastAlertToIncident.timestamp >= seven_days_ago, ) .filter(Rule.tenant_id == tenant_id) # Filter by tenant_id .group_by( @@ -2835,15 +2832,15 @@ def assign_alert_to_incident( def is_alert_assigned_to_incident( - alert_id: UUID, incident_id: UUID, tenant_id: str + fingerprint: str, incident_id: UUID, tenant_id: str ) -> bool: with Session(engine) as session: assigned = session.exec( - select(AlertToIncident) - .where(AlertToIncident.alert_id == alert_id) - .where(AlertToIncident.incident_id == incident_id) - .where(AlertToIncident.tenant_id == tenant_id) - .where(AlertToIncident.deleted_at == NULL_FOR_DELETED_AT) + select(LastAlertToIncident) + .where(LastAlertToIncident.fingerprint == fingerprint) + .where(LastAlertToIncident.incident_id == incident_id) + .where(LastAlertToIncident.tenant_id == tenant_id) + .where(LastAlertToIncident.deleted_at == NULL_FOR_DELETED_AT) ).first() return assigned is not None @@ -3360,10 +3357,10 @@ def delete_incident_by_id( # Delete all associations with alerts: ( - session.query(AlertToIncident) + session.query(LastAlertToIncident) .where( - AlertToIncident.tenant_id == tenant_id, - AlertToIncident.incident_id == incident.id, + LastAlertToIncident.tenant_id == tenant_id, + LastAlertToIncident.incident_id == incident.id, ) .delete() ) @@ -3393,7 +3390,7 @@ def get_incident_alerts_and_links_by_incident_id( offset: Optional[int] = 0, session: Optional[Session] = None, include_unlinked: bool = False, -) -> tuple[List[tuple[Alert, AlertToIncident]], int]: +) -> tuple[List[tuple[Alert, LastAlertToIncident]], int]: with existed_or_new_session(session) as session: query = ( @@ -3426,7 +3423,7 @@ def get_incident_alerts_and_links_by_incident_id( def get_incident_alerts_by_incident_id(*args, **kwargs) -> tuple[List[Alert], int]: """ - Unpacking (List[(Alert, AlertToIncident)], int) to (List[Alert], int). + Unpacking (List[(Alert, LastAlertToIncident)], int) to (List[Alert], int). """ alerts_and_links, total_alerts = get_incident_alerts_and_links_by_incident_id( *args, **kwargs @@ -3757,8 +3754,8 @@ def remove_alerts_to_incident_by_incident_id( .join(LastAlertToIncident, LastAlert.fingerprint == LastAlertToIncident.fingerprint) .join(Alert, LastAlert.alert_id == Alert.id) .filter( - AlertToIncident.deleted_at == NULL_FOR_DELETED_AT, - AlertToIncident.incident_id == incident_id, + LastAlertToIncident.deleted_at == NULL_FOR_DELETED_AT, + LastAlertToIncident.incident_id == incident_id, col(Alert.provider_type).in_(alerts_data_for_incident["sources"]), ) ) @@ -4226,12 +4223,13 @@ def get_workflow_executions_for_incident_or_alert( # Query for workflow executions associated with alerts tied to the incident alert_query = ( base_query.join( - Alert, WorkflowToAlertExecution.alert_fingerprint == Alert.fingerprint + LastAlert, WorkflowToAlertExecution.alert_fingerprint == LastAlert.fingerprint ) - .join(AlertToIncident, Alert.id == AlertToIncident.alert_id) + .join(Alert, LastAlert.alert_id == Alert.id) + .join(LastAlertToIncident, Alert.fingerprint == LastAlertToIncident.fingerprint) .where( - AlertToIncident.deleted_at == NULL_FOR_DELETED_AT, - AlertToIncident.incident_id == incident_id, + LastAlertToIncident.deleted_at == NULL_FOR_DELETED_AT, + LastAlertToIncident.incident_id == incident_id, ) ) @@ -4382,11 +4380,12 @@ def get_alerts_metrics_by_provider( Alert.provider_id, func.count(Alert.id).label("total_alerts"), func.sum( - case([(AlertToIncident.alert_id.isnot(None), 1)], else_=0) + case([(LastAlertToIncident.fingerprint.isnot(None), 1)], else_=0) ).label("correlated_alerts"), *dynamic_field_sums, ) - .outerjoin(AlertToIncident, Alert.id == AlertToIncident.alert_id) + .join(LastAlert, Alert.id == LastAlert.alert_id) + .outerjoin(LastAlertToIncident, LastAlert.fingerprint == LastAlertToIncident.fingerprint) .filter( Alert.tenant_id == tenant_id, ) diff --git a/keep/api/models/db/alert.py b/keep/api/models/db/alert.py index 86449a5a5..9e80f9599 100644 --- a/keep/api/models/db/alert.py +++ b/keep/api/models/db/alert.py @@ -10,7 +10,7 @@ from sqlalchemy.dialects.mysql import DATETIME as MySQL_DATETIME from sqlalchemy.engine.url import make_url from sqlalchemy_utils import UUIDType -from sqlmodel import JSON, TEXT, Column, DateTime, Field, Index, Relationship, SQLModel, Session +from sqlmodel import JSON, TEXT, Column, DateTime, Field, Index, Relationship, SQLModel from keep.api.consts import RUNNING_IN_CLOUD_RUN from keep.api.core.config import config diff --git a/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py b/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py index dd7814ff6..528d49986 100644 --- a/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py +++ b/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py @@ -5,16 +5,12 @@ Create Date: 2024-11-05 22:48:04.733192 """ -import warnings import sqlalchemy as sa import sqlalchemy_utils import sqlmodel from alembic import op -from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Session -from sqlalchemy.sql import expression -from sqlalchemy import exc as sa_exc # revision identifiers, used by Alembic. revision = "bdae8684d0b4" diff --git a/keep/api/tasks/process_event_task.py b/keep/api/tasks/process_event_task.py index 330d42397..28ed135fa 100644 --- a/keep/api/tasks/process_event_task.py +++ b/keep/api/tasks/process_event_task.py @@ -410,7 +410,7 @@ def __handle_formatted_events( # logger.info("Adding group alerts to the workflow manager queue") # workflow_manager.insert_events(tenant_id, grouped_alerts) # logger.info("Added group alerts to the workflow manager queue") - except Exception as ex: + except Exception: logger.exception( "Failed to run rules engine", extra={ diff --git a/keep/api/utils/enrichment_helpers.py b/keep/api/utils/enrichment_helpers.py index c3af0321a..2b0ad2b62 100644 --- a/keep/api/utils/enrichment_helpers.py +++ b/keep/api/utils/enrichment_helpers.py @@ -1,6 +1,5 @@ import logging from datetime import datetime -from optparse import Option from typing import Optional from opentelemetry import trace @@ -8,7 +7,7 @@ from keep.api.core.db import existed_or_new_session from keep.api.models.alert import AlertDto, AlertStatus, AlertWithIncidentLinkMetadataDto -from keep.api.models.db.alert import Alert, AlertToIncident +from keep.api.models.db.alert import Alert, LastAlertToIncident tracer = trace.get_tracer(__name__) logger = logging.getLogger(__name__) @@ -81,7 +80,7 @@ def calculated_start_firing_time( def convert_db_alerts_to_dto_alerts( - alerts: list[Alert | tuple[Alert, AlertToIncident]], + alerts: list[Alert | tuple[Alert, LastAlertToIncident]], with_incidents: bool = False, session: Optional[Session] = None, ) -> list[AlertDto | AlertWithIncidentLinkMetadataDto]: @@ -101,7 +100,7 @@ def convert_db_alerts_to_dto_alerts( # enrich the alerts with the enrichment data for _object in alerts: - # We may have an Alert only or and Alert with an AlertToIncident + # We may have an Alert only or and Alert with an LastAlertToIncident if isinstance(_object, Alert): alert, alert_to_incident = _object, None else: From 2689b6b1dbacf3ebc4a736c21ecc9d16c6aec229 Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Wed, 13 Nov 2024 21:42:15 +0400 Subject: [PATCH 04/26] Fix incident test and joining logic in get_last_alerts --- keep/api/core/db.py | 2 +- tests/test_incidents.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/keep/api/core/db.py b/keep/api/core/db.py index fa53cbdf5..ab008d62d 100644 --- a/keep/api/core/db.py +++ b/keep/api/core/db.py @@ -1343,7 +1343,7 @@ def get_last_alerts( query = query.add_columns(incidents_subquery.c.incidents) query = query.outerjoin( - incidents_subquery, Alert.id == incidents_subquery.c.alert_id + incidents_subquery, Alert.fingerptint == incidents_subquery.c.fingerptint ) if provider_id: diff --git a/tests/test_incidents.py b/tests/test_incidents.py index cc1e32a90..c370a6394 100644 --- a/tests/test_incidents.py +++ b/tests/test_incidents.py @@ -190,11 +190,11 @@ def test_add_remove_alert_to_incidents(db_session, setup_stress_alerts_no_elasti assert len(incident_alerts) == 89 assert total_incident_alerts == 89 - assert "source_1" not in incident.sources + assert "source_1" in incident.sources # source_0 was removed together with service_1 - assert len(incident.sources) == 8 + assert len(incident.sources) == 9 assert sorted(incident.sources) == sorted( - ["source_{}".format(i) for i in range(2, 10)] + ["source_{}".format(i) for i in range(1, 10)] ) remove_alerts_to_incident_by_incident_id( From 615e683158a67238676f65dfd0858476c499807f Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Wed, 13 Nov 2024 22:19:24 +0400 Subject: [PATCH 05/26] Fix typo --- keep/api/core/db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keep/api/core/db.py b/keep/api/core/db.py index ab008d62d..b64205a0b 100644 --- a/keep/api/core/db.py +++ b/keep/api/core/db.py @@ -1343,7 +1343,7 @@ def get_last_alerts( query = query.add_columns(incidents_subquery.c.incidents) query = query.outerjoin( - incidents_subquery, Alert.fingerptint == incidents_subquery.c.fingerptint + incidents_subquery, Alert.fingerprint == incidents_subquery.c.fingerprint ) if provider_id: From 32c9aae542bf953309d1970681888a7595c80f19 Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Wed, 13 Nov 2024 23:02:10 +0400 Subject: [PATCH 06/26] Return group by to incidents_subquery --- keep/api/core/db.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/keep/api/core/db.py b/keep/api/core/db.py index b64205a0b..99043794b 100644 --- a/keep/api/core/db.py +++ b/keep/api/core/db.py @@ -1309,6 +1309,7 @@ def get_last_alerts( ).label("incidents"), ) .filter(LastAlertToIncident.deleted_at == NULL_FOR_DELETED_AT) + .group_by(LastAlertToIncident.fingerprint) .subquery() ) @@ -1322,6 +1323,7 @@ def get_last_alerts( ).label("incidents"), ) .filter(LastAlertToIncident.deleted_at == NULL_FOR_DELETED_AT) + .group_by(LastAlertToIncident.fingerprint) .subquery() ) @@ -1336,6 +1338,7 @@ def get_last_alerts( ).label("incidents"), ) .filter(LastAlertToIncident.deleted_at == NULL_FOR_DELETED_AT) + .group_by(LastAlertToIncident.fingerprint) .subquery() ) else: From f025720db13e6de16dd2bfac09b0b38bfa03ac67 Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Thu, 14 Nov 2024 15:22:58 +0400 Subject: [PATCH 07/26] Fix creation of lastalerts in setup_stress_alerts_no_elastic --- tests/conftest.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index f73b386c6..628ed70e2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -334,6 +334,7 @@ def is_elastic_responsive(host, port, user, password): basic_auth=(user, password), ) info = elastic_client._client.info() + print("Elastic still up now") return True if info else False except Exception: print("Elastic still not up") @@ -550,7 +551,16 @@ def _setup_stress_alerts_no_elastic(num_alerts): last_alerts = [] for alert in alerts: - set_last_alert(SINGLE_TENANT_UUID, alert, db_session) + last_alerts.append( + LastAlert( + tenant_id=SINGLE_TENANT_UUID, + fingerprint=alert.fingerprint, + timestamp=alert.timestamp, + alert_id=alert.id, + ) + ) + db_session.add_all(last_alerts) + db_session.commit() return alerts @@ -564,8 +574,10 @@ def setup_stress_alerts( num_alerts = request.param.get( "num_alerts", 1000 ) # Default to 1000 alerts if not specified - + start_time = time.time() alerts = setup_stress_alerts_no_elastic(num_alerts) + print(f"time taken to setup {num_alerts} alerts with db: ", time.time() - start_time) + # add all to elasticsearch alerts_dto = convert_db_alerts_to_dto_alerts(alerts) elastic_client.index_alerts(alerts_dto) From 83e7cce433b16349191a089e583889190fa02509 Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Thu, 14 Nov 2024 15:43:35 +0400 Subject: [PATCH 08/26] Update existed LastAlerts in setup_stress_alerts_no_elastic --- tests/conftest.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 628ed70e2..3a4decf4e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -549,16 +549,29 @@ def _setup_stress_alerts_no_elastic(num_alerts): db_session.add_all(alerts) db_session.commit() + existed_last_alerts = db_session.query(LastAlert).all() + existed_last_alerts_dict = { + last_alert.fingerprint: last_alert + for last_alert in existed_last_alerts + } last_alerts = [] for alert in alerts: - last_alerts.append( - LastAlert( - tenant_id=SINGLE_TENANT_UUID, - fingerprint=alert.fingerprint, - timestamp=alert.timestamp, - alert_id=alert.id, + if alert.fingerprint in existed_last_alerts_dict: + last_alert = existed_last_alerts_dict[alert.fingerprint] + last_alert.alert_id = alert.id + last_alert.timestamp=alert.timestamp + last_alerts.append( + last_alert + ) + else: + last_alerts.append( + LastAlert( + tenant_id=SINGLE_TENANT_UUID, + fingerprint=alert.fingerprint, + timestamp=alert.timestamp, + alert_id=alert.id, + ) ) - ) db_session.add_all(last_alerts) db_session.commit() From 003aab321cbffab68e259701c29d48f6b9344de2 Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Tue, 19 Nov 2024 17:49:39 +0400 Subject: [PATCH 09/26] Fix typo --- .../db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py b/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py index 528d49986..566b726ac 100644 --- a/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py +++ b/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py @@ -38,7 +38,7 @@ def populate_db(): do nothing """ - migrate_lastalerttoincodent_query = """ + migrate_lastalerttoincident_query = """ insert into lastalerttoincident (incident_id, tenant_id, timestamp, fingerprint, is_created_by_ai, deleted_at) select ati.incident_id, ati.tenant_id, ati.timestamp, lf.fingerprint, ati.is_created_by_ai, ati.deleted_at from alerttoincident as ati @@ -70,7 +70,7 @@ def populate_db(): ) as a ON alert.fingerprint = a.fingerprint and alert.timestamp = a.last_received; """ - migrate_lastalerttoincodent_query = """ + migrate_lastalerttoincident_query = """ replace into lastalerttoincident (incident_id, tenant_id, timestamp, fingerprint, is_created_by_ai, deleted_at) select ati.incident_id, ati.tenant_id, ati.timestamp, lf.fingerprint, ati.is_created_by_ai, ati.deleted_at from alerttoincident as ati @@ -88,7 +88,7 @@ def populate_db(): """ session.execute(migrate_lastalert_query) - session.execute(migrate_lastalerttoincodent_query) + session.execute(migrate_lastalerttoincident_query) def upgrade() -> None: From e3b3d9daff5fa1c533968427f99922a0b599f55c Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Tue, 19 Nov 2024 17:50:24 +0400 Subject: [PATCH 10/26] Fix migration docs --- .../db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py b/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py index 566b726ac..908495fa0 100644 --- a/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py +++ b/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py @@ -1,7 +1,7 @@ """add lastalert and lastalerttoincident table Revision ID: bdae8684d0b4 -Revises: ef0b5b0df41c +Revises: 620b6c048091 Create Date: 2024-11-05 22:48:04.733192 """ From 6359923b6f16f330497f065b931ef0f0ba7a8c80 Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Tue, 19 Nov 2024 17:53:07 +0400 Subject: [PATCH 11/26] More explicit if condition --- keep/api/routes/workflows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keep/api/routes/workflows.py b/keep/api/routes/workflows.py index 57f7422e7..6326a6b33 100644 --- a/keep/api/routes/workflows.py +++ b/keep/api/routes/workflows.py @@ -191,7 +191,7 @@ def run_workflow( # if its event that was triggered by the UI with the Modal fingerprint = event_body.get("fingerprint", "") - if fingerprint and "test-workflow" in fingerprint or not body: + if (fingerprint and "test-workflow" in fingerprint) or not body: # some random event_body["id"] = event_body.get("fingerprint", "manual-run") event_body["name"] = event_body.get("fingerprint", "manual-run") From 1b35ed74c25bbe6e11452bc0bb2fb578ea429c56 Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Tue, 19 Nov 2024 17:53:58 +0400 Subject: [PATCH 12/26] Remove unused imports --- tests/test_incidents.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_incidents.py b/tests/test_incidents.py index c370a6394..6002058f6 100644 --- a/tests/test_incidents.py +++ b/tests/test_incidents.py @@ -13,7 +13,7 @@ get_incident_by_id, get_last_incidents, merge_incidents_to_id, - remove_alerts_to_incident_by_incident_id, enrich_incidents_with_alerts, + remove_alerts_to_incident_by_incident_id, ) from keep.api.core.db_utils import get_json_extract_field from keep.api.core.dependencies import SINGLE_TENANT_UUID @@ -23,7 +23,7 @@ IncidentSeverity, IncidentStatus, ) -from keep.api.models.db.alert import Alert, AlertToIncident, LastAlertToIncident +from keep.api.models.db.alert import Alert, LastAlertToIncident from keep.api.utils.enrichment_helpers import convert_db_alerts_to_dto_alerts from tests.fixtures.client import client, test_app # noqa From 4bc3822da1fa34e0991c8e6946d6e1fc80d39965 Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Tue, 12 Nov 2024 22:12:57 +0400 Subject: [PATCH 13/26] Add LastAlert and LastAlertToIncident and adjust incidents logic to it --- keep/api/core/db.py | 2 +- keep/api/models/db/alert.py | 2 +- .../versions/2024-11-05-22-48_bdae8684d0b4.py | 180 ++++++++++++++++++ keep/api/tasks/process_event_task.py | 2 +- tests/test_incidents.py | 8 +- 5 files changed, 187 insertions(+), 7 deletions(-) create mode 100644 keep/api/models/db/migrations/versions/2024-11-05-22-48_bdae8684d0b4.py diff --git a/keep/api/core/db.py b/keep/api/core/db.py index 99043794b..720404da1 100644 --- a/keep/api/core/db.py +++ b/keep/api/core/db.py @@ -30,7 +30,7 @@ null, select, union, - update, + update, asc, ) from sqlalchemy.dialects.mysql import insert as mysql_insert from sqlalchemy.dialects.postgresql import insert as pg_insert diff --git a/keep/api/models/db/alert.py b/keep/api/models/db/alert.py index 9e80f9599..86449a5a5 100644 --- a/keep/api/models/db/alert.py +++ b/keep/api/models/db/alert.py @@ -10,7 +10,7 @@ from sqlalchemy.dialects.mysql import DATETIME as MySQL_DATETIME from sqlalchemy.engine.url import make_url from sqlalchemy_utils import UUIDType -from sqlmodel import JSON, TEXT, Column, DateTime, Field, Index, Relationship, SQLModel +from sqlmodel import JSON, TEXT, Column, DateTime, Field, Index, Relationship, SQLModel, Session from keep.api.consts import RUNNING_IN_CLOUD_RUN from keep.api.core.config import config diff --git a/keep/api/models/db/migrations/versions/2024-11-05-22-48_bdae8684d0b4.py b/keep/api/models/db/migrations/versions/2024-11-05-22-48_bdae8684d0b4.py new file mode 100644 index 000000000..30d63bd3c --- /dev/null +++ b/keep/api/models/db/migrations/versions/2024-11-05-22-48_bdae8684d0b4.py @@ -0,0 +1,180 @@ +"""add lastalert and lastalerttoincident table + +Revision ID: bdae8684d0b4 +Revises: ef0b5b0df41c +Create Date: 2024-11-05 22:48:04.733192 + +""" +import warnings + +import sqlalchemy as sa +import sqlalchemy_utils +import sqlmodel +from alembic import op +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import Session +from sqlalchemy.sql import expression +from sqlalchemy import exc as sa_exc + +# revision identifiers, used by Alembic. +revision = "bdae8684d0b4" +down_revision = "ef0b5b0df41c" +branch_labels = None +depends_on = None + +migration_metadata = sa.MetaData() +# +# alert_to_incident_table = sa.Table( +# 'alerttoincident', +# migration_metadata, +# sa.Column("tenant_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False), +# sa.Column('alert_id', UUID(as_uuid=False), sa.ForeignKey('alert.id', ondelete='CASCADE'), primary_key=True), +# sa.Column('incident_id', UUID(as_uuid=False), sa.ForeignKey('incident.id', ondelete='CASCADE'), primary_key=True), +# sa.Column("timestamp", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()), +# sa.Column("is_created_by_ai", sa.Boolean(), nullable=False, server_default=expression.false()), +# sa.Column("deleted_at", sa.DateTime(), nullable=False, server_default="1000-01-01 00:00:00"), +# +# ) +# +# # The following code will shoow SA warning about dialect, so we suppress it. +# with warnings.catch_warnings(): +# warnings.simplefilter("ignore", category=sa_exc.SAWarning) +# incident_table = sa.Table( +# 'incident', +# migration_metadata, +# sa.Column('id', UUID(as_uuid=False), primary_key=True), +# sa.Column('alerts_count', sa.Integer, default=0), +# sa.Column('affected_services', sa.JSON, default_factory=list), +# sa.Column('sources', sa.JSON, default_factory=list) +# ) +# +# alert_table = sa.Table( +# 'alert', +# migration_metadata, +# sa.Column('id', UUID(as_uuid=False), primary_key=True), +# sa.Column('fingerprint', sa.String), +# sa.Column('provider_type', sa.String), +# sa.Column('event', sa.JSON) +# ) + +# +def populate_db(): + session = Session(op.get_bind()) + + if session.bind.dialect.name == "postgresql": + migrate_lastalert_query = """ + insert into lastalert (fingerprint, alert_id, timestamp) + select alert.fingerprint, alert.id as alert_id, alert.timestamp + from alert + join ( + select + alert.fingerprint, max(alert.timestamp) as last_received + from alert + group by fingerprint + ) as a ON alert.fingerprint = a.fingerprint and alert.timestamp = a.last_received + on conflict + do nothing + """ + + migrate_lastalerttoincodent_query = """ + insert into lastalerttoincident (incident_id, tenant_id, timestamp, fingerprint, is_created_by_ai, deleted_at) + select ati.incident_id, ati.tenant_id, ati.timestamp, lf.fingerprint, ati.is_created_by_ai, ati.deleted_at + from alerttoincident as ati + join + ( + select alert.id, alert.fingerprint + from alert + join ( + select + alert.fingerprint, max(alert.timestamp) as last_received + from alert + group by fingerprint + ) as a on alert.fingerprint = a.fingerprint and alert.timestamp = a.last_received + ) as lf on ati.alert_id = lf.id + on conflict + do nothing + """ + + else: + migrate_lastalert_query = """ + replace into lastalert (fingerprint, alert_id, timestamp) + select alert.fingerprint, alert.id as alert_id, alert.timestamp + from alert + join ( + select + alert.fingerprint, max(alert.timestamp) as last_received + from alert + group by fingerprint + ) as a ON alert.fingerprint = a.fingerprint and alert.timestamp = a.last_received; + """ + + migrate_lastalerttoincodent_query = """ + replace into lastalerttoincident (incident_id, tenant_id, timestamp, fingerprint, is_created_by_ai, deleted_at) + select ati.incident_id, ati.tenant_id, ati.timestamp, lf.fingerprint, ati.is_created_by_ai, ati.deleted_at + from alerttoincident as ati + join + ( + select alert.id, alert.fingerprint + from alert + join ( + select + alert.fingerprint, max(alert.timestamp) as last_received + from alert + group by fingerprint + ) as a on alert.fingerprint = a.fingerprint and alert.timestamp = a.last_received + ) as lf on ati.alert_id = lf.id + """ + + session.execute(migrate_lastalert_query) + session.execute(migrate_lastalerttoincodent_query) + + +def upgrade() -> None: + op.create_table( + "lastalert", + sa.Column("fingerprint", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("alert_id", sqlmodel.sql.sqltypes.GUID(), nullable=False), + sa.Column("timestamp", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint( + ["alert_id"], + ["alert.id"], + ), + sa.PrimaryKeyConstraint("fingerprint"), + ) + with op.batch_alter_table("lastalert", schema=None) as batch_op: + batch_op.create_index( + batch_op.f("ix_lastalert_timestamp"), ["timestamp"], unique=False + ) + + op.create_table( + "lastalerttoincident", + sa.Column( + "incident_id", + sqlalchemy_utils.types.uuid.UUIDType(binary=False), + nullable=False, + ), + sa.Column("tenant_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("timestamp", sa.DateTime(), nullable=False), + sa.Column("fingerprint", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("is_created_by_ai", sa.Boolean(), nullable=False), + sa.Column("deleted_at", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint( + ["fingerprint"], + ["lastalert.fingerprint"], + ), + sa.ForeignKeyConstraint(["incident_id"], ["incident.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["tenant_id"], + ["tenant.id"], + ), + sa.PrimaryKeyConstraint("incident_id", "fingerprint", "deleted_at"), + ) + + populate_db() + +def downgrade() -> None: + op.drop_table("lastalerttoincident") + with op.batch_alter_table("lastalert", schema=None) as batch_op: + batch_op.drop_index(batch_op.f("ix_lastalert_timestamp")) + + op.drop_table("lastalert") diff --git a/keep/api/tasks/process_event_task.py b/keep/api/tasks/process_event_task.py index 28ed135fa..330d42397 100644 --- a/keep/api/tasks/process_event_task.py +++ b/keep/api/tasks/process_event_task.py @@ -410,7 +410,7 @@ def __handle_formatted_events( # logger.info("Adding group alerts to the workflow manager queue") # workflow_manager.insert_events(tenant_id, grouped_alerts) # logger.info("Added group alerts to the workflow manager queue") - except Exception: + except Exception as ex: logger.exception( "Failed to run rules engine", extra={ diff --git a/tests/test_incidents.py b/tests/test_incidents.py index 6002058f6..c8b33e8b2 100644 --- a/tests/test_incidents.py +++ b/tests/test_incidents.py @@ -13,7 +13,7 @@ get_incident_by_id, get_last_incidents, merge_incidents_to_id, - remove_alerts_to_incident_by_incident_id, + remove_alerts_to_incident_by_incident_id, enrich_incidents_with_alerts, ) from keep.api.core.db_utils import get_json_extract_field from keep.api.core.dependencies import SINGLE_TENANT_UUID @@ -190,11 +190,11 @@ def test_add_remove_alert_to_incidents(db_session, setup_stress_alerts_no_elasti assert len(incident_alerts) == 89 assert total_incident_alerts == 89 - assert "source_1" in incident.sources + assert "source_1" not in incident.sources # source_0 was removed together with service_1 - assert len(incident.sources) == 9 + assert len(incident.sources) == 8 assert sorted(incident.sources) == sorted( - ["source_{}".format(i) for i in range(1, 10)] + ["source_{}".format(i) for i in range(2, 10)] ) remove_alerts_to_incident_by_incident_id( From 8f7e081781b1acf8a7dbe6ac4db7b1e7208c2821 Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Wed, 13 Nov 2024 16:53:52 +0400 Subject: [PATCH 14/26] Fix some uncovered issues --- .../versions/2024-11-05-22-48_bdae8684d0b4.py | 180 ------------------ .../versions/2024-11-13-22-48_bdae8684d0b4.py | 4 + 2 files changed, 4 insertions(+), 180 deletions(-) delete mode 100644 keep/api/models/db/migrations/versions/2024-11-05-22-48_bdae8684d0b4.py diff --git a/keep/api/models/db/migrations/versions/2024-11-05-22-48_bdae8684d0b4.py b/keep/api/models/db/migrations/versions/2024-11-05-22-48_bdae8684d0b4.py deleted file mode 100644 index 30d63bd3c..000000000 --- a/keep/api/models/db/migrations/versions/2024-11-05-22-48_bdae8684d0b4.py +++ /dev/null @@ -1,180 +0,0 @@ -"""add lastalert and lastalerttoincident table - -Revision ID: bdae8684d0b4 -Revises: ef0b5b0df41c -Create Date: 2024-11-05 22:48:04.733192 - -""" -import warnings - -import sqlalchemy as sa -import sqlalchemy_utils -import sqlmodel -from alembic import op -from sqlalchemy.dialects.postgresql import UUID -from sqlalchemy.orm import Session -from sqlalchemy.sql import expression -from sqlalchemy import exc as sa_exc - -# revision identifiers, used by Alembic. -revision = "bdae8684d0b4" -down_revision = "ef0b5b0df41c" -branch_labels = None -depends_on = None - -migration_metadata = sa.MetaData() -# -# alert_to_incident_table = sa.Table( -# 'alerttoincident', -# migration_metadata, -# sa.Column("tenant_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False), -# sa.Column('alert_id', UUID(as_uuid=False), sa.ForeignKey('alert.id', ondelete='CASCADE'), primary_key=True), -# sa.Column('incident_id', UUID(as_uuid=False), sa.ForeignKey('incident.id', ondelete='CASCADE'), primary_key=True), -# sa.Column("timestamp", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()), -# sa.Column("is_created_by_ai", sa.Boolean(), nullable=False, server_default=expression.false()), -# sa.Column("deleted_at", sa.DateTime(), nullable=False, server_default="1000-01-01 00:00:00"), -# -# ) -# -# # The following code will shoow SA warning about dialect, so we suppress it. -# with warnings.catch_warnings(): -# warnings.simplefilter("ignore", category=sa_exc.SAWarning) -# incident_table = sa.Table( -# 'incident', -# migration_metadata, -# sa.Column('id', UUID(as_uuid=False), primary_key=True), -# sa.Column('alerts_count', sa.Integer, default=0), -# sa.Column('affected_services', sa.JSON, default_factory=list), -# sa.Column('sources', sa.JSON, default_factory=list) -# ) -# -# alert_table = sa.Table( -# 'alert', -# migration_metadata, -# sa.Column('id', UUID(as_uuid=False), primary_key=True), -# sa.Column('fingerprint', sa.String), -# sa.Column('provider_type', sa.String), -# sa.Column('event', sa.JSON) -# ) - -# -def populate_db(): - session = Session(op.get_bind()) - - if session.bind.dialect.name == "postgresql": - migrate_lastalert_query = """ - insert into lastalert (fingerprint, alert_id, timestamp) - select alert.fingerprint, alert.id as alert_id, alert.timestamp - from alert - join ( - select - alert.fingerprint, max(alert.timestamp) as last_received - from alert - group by fingerprint - ) as a ON alert.fingerprint = a.fingerprint and alert.timestamp = a.last_received - on conflict - do nothing - """ - - migrate_lastalerttoincodent_query = """ - insert into lastalerttoincident (incident_id, tenant_id, timestamp, fingerprint, is_created_by_ai, deleted_at) - select ati.incident_id, ati.tenant_id, ati.timestamp, lf.fingerprint, ati.is_created_by_ai, ati.deleted_at - from alerttoincident as ati - join - ( - select alert.id, alert.fingerprint - from alert - join ( - select - alert.fingerprint, max(alert.timestamp) as last_received - from alert - group by fingerprint - ) as a on alert.fingerprint = a.fingerprint and alert.timestamp = a.last_received - ) as lf on ati.alert_id = lf.id - on conflict - do nothing - """ - - else: - migrate_lastalert_query = """ - replace into lastalert (fingerprint, alert_id, timestamp) - select alert.fingerprint, alert.id as alert_id, alert.timestamp - from alert - join ( - select - alert.fingerprint, max(alert.timestamp) as last_received - from alert - group by fingerprint - ) as a ON alert.fingerprint = a.fingerprint and alert.timestamp = a.last_received; - """ - - migrate_lastalerttoincodent_query = """ - replace into lastalerttoincident (incident_id, tenant_id, timestamp, fingerprint, is_created_by_ai, deleted_at) - select ati.incident_id, ati.tenant_id, ati.timestamp, lf.fingerprint, ati.is_created_by_ai, ati.deleted_at - from alerttoincident as ati - join - ( - select alert.id, alert.fingerprint - from alert - join ( - select - alert.fingerprint, max(alert.timestamp) as last_received - from alert - group by fingerprint - ) as a on alert.fingerprint = a.fingerprint and alert.timestamp = a.last_received - ) as lf on ati.alert_id = lf.id - """ - - session.execute(migrate_lastalert_query) - session.execute(migrate_lastalerttoincodent_query) - - -def upgrade() -> None: - op.create_table( - "lastalert", - sa.Column("fingerprint", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column("alert_id", sqlmodel.sql.sqltypes.GUID(), nullable=False), - sa.Column("timestamp", sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint( - ["alert_id"], - ["alert.id"], - ), - sa.PrimaryKeyConstraint("fingerprint"), - ) - with op.batch_alter_table("lastalert", schema=None) as batch_op: - batch_op.create_index( - batch_op.f("ix_lastalert_timestamp"), ["timestamp"], unique=False - ) - - op.create_table( - "lastalerttoincident", - sa.Column( - "incident_id", - sqlalchemy_utils.types.uuid.UUIDType(binary=False), - nullable=False, - ), - sa.Column("tenant_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column("timestamp", sa.DateTime(), nullable=False), - sa.Column("fingerprint", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column("is_created_by_ai", sa.Boolean(), nullable=False), - sa.Column("deleted_at", sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint( - ["fingerprint"], - ["lastalert.fingerprint"], - ), - sa.ForeignKeyConstraint(["incident_id"], ["incident.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint( - ["tenant_id"], - ["tenant.id"], - ), - sa.PrimaryKeyConstraint("incident_id", "fingerprint", "deleted_at"), - ) - - populate_db() - -def downgrade() -> None: - op.drop_table("lastalerttoincident") - with op.batch_alter_table("lastalert", schema=None) as batch_op: - batch_op.drop_index(batch_op.f("ix_lastalert_timestamp")) - - op.drop_table("lastalert") diff --git a/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py b/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py index 908495fa0..ee3a5bf32 100644 --- a/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py +++ b/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py @@ -5,12 +5,16 @@ Create Date: 2024-11-05 22:48:04.733192 """ +import warnings import sqlalchemy as sa import sqlalchemy_utils import sqlmodel from alembic import op +from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Session +from sqlalchemy.sql import expression +from sqlalchemy import exc as sa_exc # revision identifiers, used by Alembic. revision = "bdae8684d0b4" From 18dc03da1e850b77e51afc225c311d3324291e68 Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Wed, 13 Nov 2024 17:05:54 +0400 Subject: [PATCH 15/26] Remove unused imports and lost usages of AlertToIncident --- keep/api/core/db.py | 2 +- keep/api/models/db/alert.py | 2 +- .../db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py | 4 ---- keep/api/tasks/process_event_task.py | 2 +- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/keep/api/core/db.py b/keep/api/core/db.py index 720404da1..99043794b 100644 --- a/keep/api/core/db.py +++ b/keep/api/core/db.py @@ -30,7 +30,7 @@ null, select, union, - update, asc, + update, ) from sqlalchemy.dialects.mysql import insert as mysql_insert from sqlalchemy.dialects.postgresql import insert as pg_insert diff --git a/keep/api/models/db/alert.py b/keep/api/models/db/alert.py index 86449a5a5..9e80f9599 100644 --- a/keep/api/models/db/alert.py +++ b/keep/api/models/db/alert.py @@ -10,7 +10,7 @@ from sqlalchemy.dialects.mysql import DATETIME as MySQL_DATETIME from sqlalchemy.engine.url import make_url from sqlalchemy_utils import UUIDType -from sqlmodel import JSON, TEXT, Column, DateTime, Field, Index, Relationship, SQLModel, Session +from sqlmodel import JSON, TEXT, Column, DateTime, Field, Index, Relationship, SQLModel from keep.api.consts import RUNNING_IN_CLOUD_RUN from keep.api.core.config import config diff --git a/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py b/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py index ee3a5bf32..908495fa0 100644 --- a/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py +++ b/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py @@ -5,16 +5,12 @@ Create Date: 2024-11-05 22:48:04.733192 """ -import warnings import sqlalchemy as sa import sqlalchemy_utils import sqlmodel from alembic import op -from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Session -from sqlalchemy.sql import expression -from sqlalchemy import exc as sa_exc # revision identifiers, used by Alembic. revision = "bdae8684d0b4" diff --git a/keep/api/tasks/process_event_task.py b/keep/api/tasks/process_event_task.py index 330d42397..28ed135fa 100644 --- a/keep/api/tasks/process_event_task.py +++ b/keep/api/tasks/process_event_task.py @@ -410,7 +410,7 @@ def __handle_formatted_events( # logger.info("Adding group alerts to the workflow manager queue") # workflow_manager.insert_events(tenant_id, grouped_alerts) # logger.info("Added group alerts to the workflow manager queue") - except Exception as ex: + except Exception: logger.exception( "Failed to run rules engine", extra={ From bc73a3fcad350fbfad99b06cbbb32cc84239e348 Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Wed, 13 Nov 2024 21:42:15 +0400 Subject: [PATCH 16/26] Fix incident test and joining logic in get_last_alerts --- tests/test_incidents.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_incidents.py b/tests/test_incidents.py index c8b33e8b2..bf47fcff9 100644 --- a/tests/test_incidents.py +++ b/tests/test_incidents.py @@ -190,11 +190,11 @@ def test_add_remove_alert_to_incidents(db_session, setup_stress_alerts_no_elasti assert len(incident_alerts) == 89 assert total_incident_alerts == 89 - assert "source_1" not in incident.sources + assert "source_1" in incident.sources # source_0 was removed together with service_1 - assert len(incident.sources) == 8 + assert len(incident.sources) == 9 assert sorted(incident.sources) == sorted( - ["source_{}".format(i) for i in range(2, 10)] + ["source_{}".format(i) for i in range(1, 10)] ) remove_alerts_to_incident_by_incident_id( From 430022a7b823b57726159114900cb76c623868ef Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Tue, 19 Nov 2024 17:53:58 +0400 Subject: [PATCH 17/26] Remove unused imports --- tests/test_incidents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_incidents.py b/tests/test_incidents.py index bf47fcff9..6002058f6 100644 --- a/tests/test_incidents.py +++ b/tests/test_incidents.py @@ -13,7 +13,7 @@ get_incident_by_id, get_last_incidents, merge_incidents_to_id, - remove_alerts_to_incident_by_incident_id, enrich_incidents_with_alerts, + remove_alerts_to_incident_by_incident_id, ) from keep.api.core.db_utils import get_json_extract_field from keep.api.core.dependencies import SINGLE_TENANT_UUID From 1beb14c9a74826d0a1a2dee6cc3b0782148e1d2b Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Thu, 21 Nov 2024 11:49:22 +0400 Subject: [PATCH 18/26] Update keep/api/core/db.py Co-authored-by: Kirill Chernakov Signed-off-by: Vladimir Filonov --- keep/api/core/db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keep/api/core/db.py b/keep/api/core/db.py index 99043794b..a97f11a7c 100644 --- a/keep/api/core/db.py +++ b/keep/api/core/db.py @@ -4565,7 +4565,7 @@ def get_activity_report( def get_last_alert_by_fingerprint( tenant_id: str, fingerprint: str, session: Optional[Session] = None -) -> Optional[Alert]: +) -> Optional[LastAlert]: with existed_or_new_session(session) as session: return session.exec( select(LastAlert) From 6708fb210809e79357e8f6340315ece29ea3db92 Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Sun, 24 Nov 2024 13:11:12 +0400 Subject: [PATCH 19/26] Fix foreign key in lastalerttoincident for complex primary key in lastalert --- keep/api/models/db/alert.py | 2 +- .../versions/2024-11-13-22-48_bdae8684d0b4.py | 82 +++++++++++-------- 2 files changed, 49 insertions(+), 35 deletions(-) diff --git a/keep/api/models/db/alert.py b/keep/api/models/db/alert.py index 9e80f9599..a31a3089b 100644 --- a/keep/api/models/db/alert.py +++ b/keep/api/models/db/alert.py @@ -82,7 +82,7 @@ class LastAlert(SQLModel, table=True): class LastAlertToIncident(SQLModel, table=True): - tenant_id: str = Field(foreign_key="tenant.id", nullable=False) + tenant_id: str = Field(foreign_key="tenant.id", nullable=False, primary_key=True) timestamp: datetime = Field(default_factory=datetime.utcnow) fingerprint: str = Field(foreign_key="lastalert.fingerprint", primary_key=True) diff --git a/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py b/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py index 908495fa0..587f072f9 100644 --- a/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py +++ b/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py @@ -20,6 +20,7 @@ migration_metadata = sa.MetaData() + def populate_db(): session = Session(op.get_bind()) @@ -32,8 +33,8 @@ def populate_db(): select alert.fingerprint, max(alert.timestamp) as last_received from alert - group by fingerprint - ) as a ON alert.fingerprint = a.fingerprint and alert.timestamp = a.last_received + group by fingerprint, tenant_id + ) as a ON alert.fingerprint = a.fingerprint and alert.timestamp = a.last_received and alert.tenant_id = a.tenant_id on conflict do nothing """ @@ -44,14 +45,14 @@ def populate_db(): from alerttoincident as ati join ( - select alert.id, alert.fingerprint + select alert.tenant_id, alert.id, alert.fingerprint from alert join ( select - alert.fingerprint, max(alert.timestamp) as last_received + alert.tenant_id, alert.fingerprint, max(alert.timestamp) as last_received from alert - group by fingerprint - ) as a on alert.fingerprint = a.fingerprint and alert.timestamp = a.last_received + group by fingerprint, tenant_id + ) as a on alert.fingerprint = a.fingerprint and alert.timestamp = a.last_received and alert.tenant_id = a.tenant_id ) as lf on ati.alert_id = lf.id on conflict do nothing @@ -59,33 +60,43 @@ def populate_db(): else: migrate_lastalert_query = """ - replace into lastalert (tenant_id, fingerprint, alert_id, timestamp) - select alert.tenant_id, alert.fingerprint, alert.id as alert_id, alert.timestamp + INSERT INTO lastalert (tenant_id, fingerprint, alert_id, timestamp) + SELECT + grouped_alerts.tenant_id, + grouped_alerts.fingerprint, + MAX(grouped_alerts.alert_id) as alert_id, -- Using MAX to consistently pick one alert_id + grouped_alerts.timestamp + FROM ( + select alert.tenant_id, alert.fingerprint, alert.id as alert_id, alert.timestamp + from alert + join ( + select + alert.tenant_id, alert.fingerprint, max(alert.timestamp) as last_received from alert - join ( - select - alert.fingerprint, max(alert.timestamp) as last_received - from alert - group by fingerprint - ) as a ON alert.fingerprint = a.fingerprint and alert.timestamp = a.last_received; - """ + group by fingerprint, tenant_id + ) as a ON alert.fingerprint = a.fingerprint + and alert.timestamp = a.last_received + and alert.tenant_id = a.tenant_id + ) as grouped_alerts + GROUP BY grouped_alerts.tenant_id, grouped_alerts.fingerprint, grouped_alerts.timestamp; +""" migrate_lastalerttoincident_query = """ - replace into lastalerttoincident (incident_id, tenant_id, timestamp, fingerprint, is_created_by_ai, deleted_at) - select ati.incident_id, ati.tenant_id, ati.timestamp, lf.fingerprint, ati.is_created_by_ai, ati.deleted_at - from alerttoincident as ati - join - ( - select alert.id, alert.fingerprint + REPLACE INTO lastalerttoincident (incident_id, tenant_id, timestamp, fingerprint, is_created_by_ai, deleted_at) + select ati.incident_id, ati.tenant_id, ati.timestamp, lf.fingerprint, ati.is_created_by_ai, ati.deleted_at + from alerttoincident as ati + join + ( + select alert.id, alert.fingerprint, alert.tenant_id + from alert + join ( + select + alert.tenant_id,alert.fingerprint, max(alert.timestamp) as last_received from alert - join ( - select - alert.fingerprint, max(alert.timestamp) as last_received - from alert - group by fingerprint - ) as a on alert.fingerprint = a.fingerprint and alert.timestamp = a.last_received - ) as lf on ati.alert_id = lf.id - """ + group by fingerprint, tenant_id + ) as a on alert.fingerprint = a.fingerprint and alert.timestamp = a.last_received and alert.tenant_id = a.tenant_id + ) as lf on ati.alert_id = lf.id; + """ session.execute(migrate_lastalert_query) session.execute(migrate_lastalerttoincident_query) @@ -102,12 +113,14 @@ def upgrade() -> None: ["alert_id"], ["alert.id"], ), - sa.PrimaryKeyConstraint("fingerprint"), + sa.PrimaryKeyConstraint("tenant_id", "fingerprint"), ) with op.batch_alter_table("lastalert", schema=None) as batch_op: batch_op.create_index( batch_op.f("ix_lastalert_timestamp"), ["timestamp"], unique=False ) + # Add index for the fingerprint column that will be referenced by foreign key + batch_op.create_index("ix_lastalert_fingerprint", ["fingerprint"], unique=False) op.create_table( "lastalerttoincident", @@ -122,22 +135,23 @@ def upgrade() -> None: sa.Column("is_created_by_ai", sa.Boolean(), nullable=False), sa.Column("deleted_at", sa.DateTime(), nullable=True), sa.ForeignKeyConstraint( - ["fingerprint"], - ["lastalert.fingerprint"], + ["tenant_id", "fingerprint"], + ["lastalert.tenant_id", "lastalert.fingerprint"], ), sa.ForeignKeyConstraint(["incident_id"], ["incident.id"], ondelete="CASCADE"), sa.ForeignKeyConstraint( ["tenant_id"], ["tenant.id"], ), - sa.PrimaryKeyConstraint("incident_id", "fingerprint", "deleted_at"), + sa.PrimaryKeyConstraint("tenant_id", "incident_id", "fingerprint", "deleted_at"), ) populate_db() + def downgrade() -> None: op.drop_table("lastalerttoincident") with op.batch_alter_table("lastalert", schema=None) as batch_op: batch_op.drop_index(batch_op.f("ix_lastalert_timestamp")) - op.drop_table("lastalert") + op.drop_table("lastalert") \ No newline at end of file From 321f477a7fcf85d96e608bff859c7f1026ce8f37 Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Sun, 24 Nov 2024 15:42:31 +0400 Subject: [PATCH 20/26] Fix groupping in Rule histogram query and migaration deps --- keep/api/core/db.py | 4 ++-- ...22-48_bdae8684d0b4.py => 2024-11-24-22-48_bdae8684d0b4.py} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename keep/api/models/db/migrations/versions/{2024-11-13-22-48_bdae8684d0b4.py => 2024-11-24-22-48_bdae8684d0b4.py} (97%) diff --git a/keep/api/core/db.py b/keep/api/core/db.py index a97f11a7c..c600c1214 100644 --- a/keep/api/core/db.py +++ b/keep/api/core/db.py @@ -1808,7 +1808,7 @@ def get_rule_distribution(tenant_id, minute=False): session.query( Rule.id.label("rule_id"), Rule.name.label("rule_name"), - Incident.id.label("group_id"), + Incident.id.label("incident_id"), Incident.rule_fingerprint.label("rule_fingerprint"), timestamp_format.label("time"), func.count(LastAlertToIncident.fingerprint).label("hits"), @@ -1821,7 +1821,7 @@ def get_rule_distribution(tenant_id, minute=False): ) .filter(Rule.tenant_id == tenant_id) # Filter by tenant_id .group_by( - "rule_id", "rule_name", "incident_id", "rule_fingerprint", "time" + Rule.id, "rule_name", Incident.id, "rule_fingerprint", "time" ) # Adjusted here .order_by("time") ) diff --git a/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py b/keep/api/models/db/migrations/versions/2024-11-24-22-48_bdae8684d0b4.py similarity index 97% rename from keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py rename to keep/api/models/db/migrations/versions/2024-11-24-22-48_bdae8684d0b4.py index 587f072f9..6473a534d 100644 --- a/keep/api/models/db/migrations/versions/2024-11-13-22-48_bdae8684d0b4.py +++ b/keep/api/models/db/migrations/versions/2024-11-24-22-48_bdae8684d0b4.py @@ -14,7 +14,7 @@ # revision identifiers, used by Alembic. revision = "bdae8684d0b4" -down_revision = "620b6c048091" +down_revision = "192157fd5788" branch_labels = None depends_on = None @@ -31,7 +31,7 @@ def populate_db(): from alert join ( select - alert.fingerprint, max(alert.timestamp) as last_received + alert.tenant_id, alert.fingerprint, max(alert.timestamp) as last_received from alert group by fingerprint, tenant_id ) as a ON alert.fingerprint = a.fingerprint and alert.timestamp = a.last_received and alert.tenant_id = a.tenant_id From 0d80046abd8c98772b41ac45392a8332c4eb8c5b Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Sun, 24 Nov 2024 20:41:40 +0400 Subject: [PATCH 21/26] LastAlert.tenant_id should be primary key not only in migration --- keep/api/models/db/alert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keep/api/models/db/alert.py b/keep/api/models/db/alert.py index a31a3089b..c71562945 100644 --- a/keep/api/models/db/alert.py +++ b/keep/api/models/db/alert.py @@ -75,7 +75,7 @@ class AlertToIncident(SQLModel, table=True): class LastAlert(SQLModel, table=True): - tenant_id: str = Field(foreign_key="tenant.id", nullable=False) + tenant_id: str = Field(foreign_key="tenant.id", nullable=False, primary_key=True) fingerprint: str = Field(primary_key=True) alert_id: UUID = Field(foreign_key="alert.id") timestamp: datetime = Field(nullable=False, index=True) From cccac1143bcf8931c2fb425ef2132b6341a87023 Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Sun, 24 Nov 2024 21:26:29 +0400 Subject: [PATCH 22/26] Clean up FKs and indexes for lastalert --- keep/api/models/db/alert.py | 14 +++++++++++--- .../versions/2024-11-24-22-48_bdae8684d0b4.py | 4 ++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/keep/api/models/db/alert.py b/keep/api/models/db/alert.py index c71562945..07f623030 100644 --- a/keep/api/models/db/alert.py +++ b/keep/api/models/db/alert.py @@ -5,7 +5,7 @@ from uuid import UUID, uuid4 from pydantic import PrivateAttr -from sqlalchemy import ForeignKey, UniqueConstraint +from sqlalchemy import ForeignKey, UniqueConstraint, ForeignKeyConstraint from sqlalchemy.dialects.mssql import DATETIME2 as MSSQL_DATETIME2 from sqlalchemy.dialects.mysql import DATETIME as MySQL_DATETIME from sqlalchemy.engine.url import make_url @@ -76,7 +76,7 @@ class AlertToIncident(SQLModel, table=True): class LastAlert(SQLModel, table=True): tenant_id: str = Field(foreign_key="tenant.id", nullable=False, primary_key=True) - fingerprint: str = Field(primary_key=True) + fingerprint: str = Field(primary_key=True, index=True) alert_id: UUID = Field(foreign_key="alert.id") timestamp: datetime = Field(nullable=False, index=True) @@ -85,7 +85,7 @@ class LastAlertToIncident(SQLModel, table=True): tenant_id: str = Field(foreign_key="tenant.id", nullable=False, primary_key=True) timestamp: datetime = Field(default_factory=datetime.utcnow) - fingerprint: str = Field(foreign_key="lastalert.fingerprint", primary_key=True) + fingerprint: str = Field(primary_key=True) incident_id: UUID = Field( sa_column=Column( UUIDType(binary=False), @@ -103,6 +103,14 @@ class LastAlertToIncident(SQLModel, table=True): default=NULL_FOR_DELETED_AT, ) + __table_args__ = ( + ForeignKeyConstraint( + ["tenant_id", "fingerprint"], + ["lastalert.tenant_id", "lastalert.fingerprint"]), + {} + ) + + # alert: "Alert" = Relationship( # back_populates="alert_to_incident_link", # sa_relationship = relationship( diff --git a/keep/api/models/db/migrations/versions/2024-11-24-22-48_bdae8684d0b4.py b/keep/api/models/db/migrations/versions/2024-11-24-22-48_bdae8684d0b4.py index 6473a534d..ccb01c1ac 100644 --- a/keep/api/models/db/migrations/versions/2024-11-24-22-48_bdae8684d0b4.py +++ b/keep/api/models/db/migrations/versions/2024-11-24-22-48_bdae8684d0b4.py @@ -113,6 +113,10 @@ def upgrade() -> None: ["alert_id"], ["alert.id"], ), + sa.ForeignKeyConstraint( + ["tenant_id"], + ["tenant.id"], + ), sa.PrimaryKeyConstraint("tenant_id", "fingerprint"), ) with op.batch_alter_table("lastalert", schema=None) as batch_op: From 76135a9879c8fd13d875c6f9a8fe3102aa4ecf17 Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Mon, 2 Dec 2024 13:06:42 +0400 Subject: [PATCH 23/26] Remove unused imports --- .../db/migrations/versions/2024-12-01-16-40_3ad5308e7200.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/keep/api/models/db/migrations/versions/2024-12-01-16-40_3ad5308e7200.py b/keep/api/models/db/migrations/versions/2024-12-01-16-40_3ad5308e7200.py index f92d246a8..6290961d9 100644 --- a/keep/api/models/db/migrations/versions/2024-12-01-16-40_3ad5308e7200.py +++ b/keep/api/models/db/migrations/versions/2024-12-01-16-40_3ad5308e7200.py @@ -7,10 +7,7 @@ """ import sqlalchemy as sa -import sqlalchemy_utils -import sqlmodel from alembic import op -from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. revision = "3ad5308e7200" From 0d484db7f8f4a519786926460c9f465d016d483b Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Mon, 2 Dec 2024 13:41:37 +0400 Subject: [PATCH 24/26] Resolve migration conflicts --- .../db/migrations/versions/2024-12-01-16-40_3ad5308e7200.py | 2 +- ...22-48_bdae8684d0b4.py => 2024-12-02-13-36_bdae8684d0b4.py} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename keep/api/models/db/migrations/versions/{2024-11-24-22-48_bdae8684d0b4.py => 2024-12-02-13-36_bdae8684d0b4.py} (99%) diff --git a/keep/api/models/db/migrations/versions/2024-12-01-16-40_3ad5308e7200.py b/keep/api/models/db/migrations/versions/2024-12-01-16-40_3ad5308e7200.py index 6290961d9..5d8dcb0cf 100644 --- a/keep/api/models/db/migrations/versions/2024-12-01-16-40_3ad5308e7200.py +++ b/keep/api/models/db/migrations/versions/2024-12-01-16-40_3ad5308e7200.py @@ -1,7 +1,7 @@ """New types for AI config Revision ID: 3ad5308e7200 -Revises: 3f056d747d9e +Revises: 192157fd5788 Create Date: 2024-12-01 16:40:12.655642 """ diff --git a/keep/api/models/db/migrations/versions/2024-11-24-22-48_bdae8684d0b4.py b/keep/api/models/db/migrations/versions/2024-12-02-13-36_bdae8684d0b4.py similarity index 99% rename from keep/api/models/db/migrations/versions/2024-11-24-22-48_bdae8684d0b4.py rename to keep/api/models/db/migrations/versions/2024-12-02-13-36_bdae8684d0b4.py index ccb01c1ac..905748deb 100644 --- a/keep/api/models/db/migrations/versions/2024-11-24-22-48_bdae8684d0b4.py +++ b/keep/api/models/db/migrations/versions/2024-12-02-13-36_bdae8684d0b4.py @@ -1,7 +1,7 @@ """add lastalert and lastalerttoincident table Revision ID: bdae8684d0b4 -Revises: 620b6c048091 +Revises: 3ad5308e7200 Create Date: 2024-11-05 22:48:04.733192 """ @@ -14,7 +14,7 @@ # revision identifiers, used by Alembic. revision = "bdae8684d0b4" -down_revision = "192157fd5788" +down_revision = "3ad5308e7200" branch_labels = None depends_on = None From d69b96aaf19b39f095d13efcefbe9e14d0fcca17 Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Mon, 2 Dec 2024 14:53:49 +0400 Subject: [PATCH 25/26] Fix alter column settings for json --- .../db/migrations/versions/2024-12-01-16-40_3ad5308e7200.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/keep/api/models/db/migrations/versions/2024-12-01-16-40_3ad5308e7200.py b/keep/api/models/db/migrations/versions/2024-12-01-16-40_3ad5308e7200.py index 5d8dcb0cf..252096a6f 100644 --- a/keep/api/models/db/migrations/versions/2024-12-01-16-40_3ad5308e7200.py +++ b/keep/api/models/db/migrations/versions/2024-12-01-16-40_3ad5308e7200.py @@ -21,13 +21,15 @@ def upgrade() -> None: with op.batch_alter_table("externalaiconfigandmetadata", schema=None) as batch_op: batch_op.alter_column( - "settings", existing_type=sa.VARCHAR(), type_=sa.JSON(), nullable=True + "settings", existing_type=sa.VARCHAR(), type_=sa.JSON(), nullable=True, + postgresql_using="settings::json" ) batch_op.alter_column( "settings_proposed_by_algorithm", existing_type=sa.VARCHAR(), type_=sa.JSON(), existing_nullable=True, + postgresql_using="settings::json" ) batch_op.alter_column( "feedback_logs", From 940840054511b3d3546cb2343ec126cfd22588aa Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Mon, 2 Dec 2024 18:57:28 +0400 Subject: [PATCH 26/26] clean leftovers --- keep/api/models/db/alert.py | 21 ------------------- .../versions/2024-12-01-16-40_3ad5308e7200.py | 2 +- tests/conftest.py | 4 ---- 3 files changed, 1 insertion(+), 26 deletions(-) diff --git a/keep/api/models/db/alert.py b/keep/api/models/db/alert.py index 07f623030..0d0bac569 100644 --- a/keep/api/models/db/alert.py +++ b/keep/api/models/db/alert.py @@ -61,8 +61,6 @@ class AlertToIncident(SQLModel, table=True): primary_key=True, ) ) - # alert: "Alert" = Relationship(back_populates="alert_to_incident_link") - # incident: "Incident" = Relationship(back_populates="alert_to_incident_link") is_created_by_ai: bool = Field(default=False) @@ -111,20 +109,6 @@ class LastAlertToIncident(SQLModel, table=True): ) - # alert: "Alert" = Relationship( - # back_populates="alert_to_incident_link", - # sa_relationship = relationship( - # "Alert", - # secondary="lastalert", - # primaryjoin=f"""LastAlertToIncident.fingerprint == LastAlert.fingerprint""", - # secondaryjoin="LastAlert.alert_id == Alert.id", - # overlaps="alert,lastalert", - # viewonly=True, - # ), - # ) - # incident: "Incident" = Relationship(back_populates="alert_to_incident_link") - - class Incident(SQLModel, table=True): id: UUID = Field(default_factory=uuid4, primary_key=True) tenant_id: str = Field(foreign_key="tenant.id") @@ -148,11 +132,6 @@ class Incident(SQLModel, table=True): end_time: datetime | None last_seen_time: datetime | None - # alert_to_incident_link: List[LastAlertToIncident] = Relationship( - # back_populates="incident", - # sa_relationship_kwargs={"overlaps": "alerts,incidents"}, - # ) - is_predicted: bool = Field(default=False) is_confirmed: bool = Field(default=False) diff --git a/keep/api/models/db/migrations/versions/2024-12-01-16-40_3ad5308e7200.py b/keep/api/models/db/migrations/versions/2024-12-01-16-40_3ad5308e7200.py index 252096a6f..e6c2140e3 100644 --- a/keep/api/models/db/migrations/versions/2024-12-01-16-40_3ad5308e7200.py +++ b/keep/api/models/db/migrations/versions/2024-12-01-16-40_3ad5308e7200.py @@ -1,7 +1,7 @@ """New types for AI config Revision ID: 3ad5308e7200 -Revises: 192157fd5788 +Revises: 3f056d747d9e Create Date: 2024-12-01 16:40:12.655642 """ diff --git a/tests/conftest.py b/tests/conftest.py index 3a4decf4e..6a353aa31 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,7 +16,6 @@ from sqlmodel import SQLModel, Session, create_engine from starlette_context import context, request_cycle_context -from keep.api.core.db import set_last_alert # This import is required to create the tables from keep.api.core.dependencies import SINGLE_TENANT_UUID from keep.api.core.elastic import ElasticClient @@ -587,10 +586,7 @@ def setup_stress_alerts( num_alerts = request.param.get( "num_alerts", 1000 ) # Default to 1000 alerts if not specified - start_time = time.time() alerts = setup_stress_alerts_no_elastic(num_alerts) - print(f"time taken to setup {num_alerts} alerts with db: ", time.time() - start_time) - # add all to elasticsearch alerts_dto = convert_db_alerts_to_dto_alerts(alerts) elastic_client.index_alerts(alerts_dto)