- Alerts
- Timeline
- Topology
+ Alerts
+ Timeline
+ Topology
- Coming Soon...
+
+
+
Coming Soon...
diff --git a/keep-ui/package-lock.json b/keep-ui/package-lock.json
index 4459999ce..ebe5dba09 100644
--- a/keep-ui/package-lock.json
+++ b/keep-ui/package-lock.json
@@ -372,6 +372,7 @@
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.12",
+ "@types/d3-time-format": "^4.0.3",
"@types/js-cookie": "^3.0.3",
"@types/js-yaml": "^4.0.5",
"@types/json-logic-js": "^2.0.7",
@@ -4022,6 +4023,12 @@
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz",
"integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw=="
},
+ "node_modules/@types/d3-time-format": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
+ "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
+ "dev": true
+ },
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
diff --git a/keep-ui/package.json b/keep-ui/package.json
index 206ea0c2e..e1c722b95 100644
--- a/keep-ui/package.json
+++ b/keep-ui/package.json
@@ -373,6 +373,7 @@
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.12",
+ "@types/d3-time-format": "^4.0.3",
"@types/js-cookie": "^3.0.3",
"@types/js-yaml": "^4.0.5",
"@types/json-logic-js": "^2.0.7",
diff --git a/keep-ui/tailwind.config.js b/keep-ui/tailwind.config.js
index aae009bf3..1646a014b 100644
--- a/keep-ui/tailwind.config.js
+++ b/keep-ui/tailwind.config.js
@@ -8,6 +8,10 @@ module.exports = {
darkMode: "class",
theme: {
extend: {
+ gridTemplateColumns: {
+ 20: "repeat(20, minmax(0, 1fr))",
+ 24: "repeat(24, minmax(0, 1fr))",
+ },
minHeight: {
"screen-minus-200": "calc(100vh - 200px)",
},
@@ -129,5 +133,8 @@ module.exports = {
/^(fill-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
},
],
- plugins: [require("@headlessui/tailwindcss"), require('@tailwindcss/typography')],
+ plugins: [
+ require("@headlessui/tailwindcss"),
+ require("@tailwindcss/typography"),
+ ],
};
diff --git a/keep-ui/utils/hooks/useAlerts.ts b/keep-ui/utils/hooks/useAlerts.ts
index 53b343b93..23d73139c 100644
--- a/keep-ui/utils/hooks/useAlerts.ts
+++ b/keep-ui/utils/hooks/useAlerts.ts
@@ -6,6 +6,15 @@ import { getApiURL } from "utils/apiUrl";
import { fetcher } from "utils/fetcher";
import { toDateObjectWithFallback } from "utils/helpers";
+export type AuditEvent = {
+ id: string;
+ user_id: string;
+ action: string;
+ description: string;
+ timestamp: string;
+ fingerprint: string;
+};
+
export const useAlerts = () => {
const apiUrl = getApiURL();
const { data: session } = useSession();
@@ -33,7 +42,8 @@ export const useAlerts = () => {
options: SWRConfiguration = { revalidateOnFocus: false }
) => {
return useSWR
(
- () => (session && presetName ? `${apiUrl}/preset/${presetName}/alerts` : null),
+ () =>
+ session && presetName ? `${apiUrl}/preset/${presetName}/alerts` : null,
(url) => fetcher(url, session?.accessToken),
options
);
@@ -78,12 +88,32 @@ export const useAlerts = () => {
};
};
+ const useMultipleFingerprintsAlertAudit = (
+ fingerprints: string[] | undefined,
+ options: SWRConfiguration = { revalidateOnFocus: true }
+ ) => {
+ return useSWR(
+ () => (session && fingerprints ? `${apiUrl}/alerts/audit` : null),
+ (url) =>
+ fetcher(url, session?.accessToken, {
+ method: "POST",
+ body: JSON.stringify(fingerprints),
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ }),
+ options
+ );
+ };
+
const useAlertAudit = (
fingerprint: string,
options: SWRConfiguration = { revalidateOnFocus: false }
) => {
- return useSWR(
- () => (session && fingerprint ? `${apiUrl}/alerts/${fingerprint}/audit` : null),
+ return useSWR(
+ () =>
+ session && fingerprint ? `${apiUrl}/alerts/${fingerprint}/audit` : null,
(url) => fetcher(url, session?.accessToken),
options
);
@@ -93,6 +123,7 @@ export const useAlerts = () => {
useAlertHistory,
useAllAlerts,
usePresetAlerts,
- useAlertAudit
+ useAlertAudit,
+ useMultipleFingerprintsAlertAudit,
};
};
diff --git a/keep-ui/utils/hooks/useTopology.ts b/keep-ui/utils/hooks/useTopology.ts
index ba959cd54..a3180815c 100644
--- a/keep-ui/utils/hooks/useTopology.ts
+++ b/keep-ui/utils/hooks/useTopology.ts
@@ -4,7 +4,7 @@ import useSWR from "swr";
import { getApiURL } from "utils/apiUrl";
import { fetcher } from "utils/fetcher";
import { useWebsocket } from "./usePusher";
-import { useCallback, useEffect } from "react";
+import { useCallback, useEffect, useState } from "react";
import { toast } from "react-toastify";
const isNullOrUndefined = (value: any) => value === null || value === undefined;
@@ -20,7 +20,7 @@ export const useTopology = (
environment?: string
) => {
const { data: session } = useSession();
- useTopologyPolling();
+ const { data: pollTopology } = useTopologyPolling();
const apiUrl = getApiURL();
const url = !session
@@ -36,6 +36,12 @@ export const useTopology = (
(url: string) => fetcher(url, session!.accessToken)
);
+ useEffect(() => {
+ if (pollTopology) {
+ mutate();
+ }
+ }, [pollTopology, mutate]);
+
return {
topologyData: data,
error,
@@ -46,12 +52,14 @@ export const useTopology = (
export const useTopologyPolling = () => {
const { bind, unbind } = useWebsocket();
+ const [pollTopology, setPollTopology] = useState(0);
const handleIncoming = useCallback((data: TopologyUpdate) => {
toast.success(
`Topology pulled from ${data.providerId} (${data.providerType})`,
{ position: "top-right" }
);
+ setPollTopology(Math.floor(Math.random() * 10000));
}, []);
useEffect(() => {
@@ -60,4 +68,6 @@ export const useTopologyPolling = () => {
unbind("topology-update", handleIncoming);
};
}, [bind, unbind, handleIncoming]);
+
+ return { data: pollTopology };
};
diff --git a/keep/api/core/db.py b/keep/api/core/db.py
index 2adf87a74..fbc21d200 100644
--- a/keep/api/core/db.py
+++ b/keep/api/core/db.py
@@ -2094,16 +2094,38 @@ def get_incidents(tenant_id) -> List[Incident]:
def get_alert_audit(
- tenant_id: str, fingerprint: str, limit: int = 50
+ tenant_id: str, fingerprint: str | list[str], limit: int = 50
) -> List[AlertAudit]:
+ """
+ Get the alert audit for the given fingerprint(s).
+
+ Args:
+ tenant_id (str): the tenant_id to filter the alert audit by
+ fingerprint (str | list[str]): the fingerprint(s) to filter the alert audit by
+ limit (int, optional): the maximum number of alert audits to return. Defaults to 50.
+
+ Returns:
+ List[AlertAudit]: the alert audit for the given fingerprint(s)
+ """
with Session(engine) as session:
- audit = session.exec(
- select(AlertAudit)
- .where(AlertAudit.tenant_id == tenant_id)
- .where(AlertAudit.fingerprint == fingerprint)
- .order_by(desc(AlertAudit.timestamp))
- .limit(limit)
- ).all()
+ if isinstance(fingerprint, list):
+ query = (
+ select(AlertAudit)
+ .where(AlertAudit.tenant_id == tenant_id)
+ .where(AlertAudit.fingerprint.in_(fingerprint))
+ .order_by(desc(AlertAudit.timestamp), AlertAudit.fingerprint)
+ )
+ if limit:
+ query = query.limit(limit)
+ audit = session.exec(query).all()
+ else:
+ audit = session.exec(
+ select(AlertAudit)
+ .where(AlertAudit.tenant_id == tenant_id)
+ .where(AlertAudit.fingerprint == fingerprint)
+ .order_by(desc(AlertAudit.timestamp))
+ .limit(limit)
+ ).all()
return audit
diff --git a/keep/api/models/alert_audit.py b/keep/api/models/alert_audit.py
new file mode 100644
index 000000000..f03a5af67
--- /dev/null
+++ b/keep/api/models/alert_audit.py
@@ -0,0 +1,57 @@
+from datetime import datetime
+
+from pydantic import BaseModel
+
+from keep.api.models.db.alert import AlertActionType, AlertAudit
+
+
+class AlertAuditDto(BaseModel):
+ id: str
+ timestamp: datetime
+ fingerprint: str
+ action: AlertActionType
+ user_id: str
+ description: str
+
+ @classmethod
+ def from_orm(cls, alert_audit: AlertAudit) -> "AlertAuditDto":
+ return cls(
+ id=str(alert_audit.id),
+ timestamp=alert_audit.timestamp,
+ fingerprint=alert_audit.fingerprint,
+ action=alert_audit.action,
+ user_id=alert_audit.user_id,
+ description=alert_audit.description,
+ )
+
+ @classmethod
+ def from_orm_list(cls, alert_audits: list[AlertAudit]) -> list["AlertAuditDto"]:
+ grouped_events = []
+ previous_event = None
+ count = 1
+
+ for event in alert_audits:
+ # Check if the current event is similar to the previous event
+ if previous_event and (
+ event.user_id == previous_event.user_id
+ and event.action == previous_event.action
+ and event.description == previous_event.description
+ ):
+ # Increment the count if the events are similar
+ count += 1
+ else:
+ # If the events are not similar, append the previous event to the grouped events
+ if previous_event:
+ if count > 1:
+ previous_event.description += f" x{count}"
+ grouped_events.append(AlertAuditDto.from_orm(previous_event))
+ # Update the previous event to the current event and reset the count
+ previous_event = event
+ count = 1
+
+ # Add the last event to the grouped events
+ if previous_event:
+ if count > 1:
+ previous_event.description += f" x{count}"
+ grouped_events.append(AlertAuditDto.from_orm(previous_event))
+ return grouped_events
diff --git a/keep/api/routes/alerts.py b/keep/api/routes/alerts.py
index 5935d63a4..210d5adea 100644
--- a/keep/api/routes/alerts.py
+++ b/keep/api/routes/alerts.py
@@ -34,6 +34,7 @@
EnrichAlertRequestBody,
UnEnrichAlertRequestBody,
)
+from keep.api.models.alert_audit import AlertAuditDto
from keep.api.models.db.alert import AlertActionType
from keep.api.models.search_alert import SearchAlertsRequest
from keep.api.tasks.process_event_task import process_event
@@ -686,16 +687,49 @@ async def search_alerts(
raise HTTPException(status_code=500, detail="Failed to search alerts")
+@router.post(
+ "/audit",
+ description="Get alert timeline audit trail for multiple fingerprints",
+)
+def get_multiple_fingerprint_alert_audit(
+ fingerprints: list[str],
+ authenticated_entity: AuthenticatedEntity = Depends(
+ IdentityManagerFactory.get_auth_verifier(["read:alert"])
+ ),
+) -> list[AlertAuditDto]:
+ tenant_id = authenticated_entity.tenant_id
+ logger.info(
+ "Fetching alert audit",
+ extra={"fingerprints": fingerprints, "tenant_id": tenant_id},
+ )
+ alert_audit = get_alert_audit_db(tenant_id, fingerprints)
+
+ if not alert_audit:
+ raise HTTPException(status_code=404, detail="Alert not found")
+ grouped_events = []
+
+ # Group the results by fingerprint for "deduplication" (2x, 3x, etc.) thingy..
+ grouped_audit = {}
+ for audit in alert_audit:
+ if audit.fingerprint not in grouped_audit:
+ grouped_audit[audit.fingerprint] = []
+ grouped_audit[audit.fingerprint].append(audit)
+
+ for values in grouped_audit.values():
+ grouped_events.extend(AlertAuditDto.from_orm_list(values))
+ return grouped_events
+
+
@router.get(
"/{fingerprint}/audit",
- description="Get alert enrichment",
+ description="Get alert timeline audit trail",
)
def get_alert_audit(
fingerprint: str,
authenticated_entity: AuthenticatedEntity = Depends(
IdentityManagerFactory.get_auth_verifier(["read:alert"])
),
-):
+) -> list[AlertAuditDto]:
tenant_id = authenticated_entity.tenant_id
logger.info(
"Fetching alert audit",
@@ -708,29 +742,5 @@ def get_alert_audit(
if not alert_audit:
raise HTTPException(status_code=404, detail="Alert not found")
- grouped_events = []
- previous_event = None
- count = 1
-
- for event in alert_audit:
- if previous_event and (
- event.user_id == previous_event.user_id
- and event.action == previous_event.action
- and event.description == previous_event.description
- ):
- count += 1
- else:
- if previous_event:
- if count > 1:
- previous_event.description += f" x{count}"
- grouped_events.append(previous_event.dict())
- previous_event = event
- count = 1
-
- # Add the last event
- if previous_event:
- if count > 1:
- previous_event.description += f" x{count}"
- grouped_events.append(previous_event.dict())
-
+ grouped_events = AlertAuditDto.from_orm_list(alert_audit)
return grouped_events
diff --git a/keep/api/utils/enrichment_helpers.py b/keep/api/utils/enrichment_helpers.py
index 7085626c0..bd2baa38c 100644
--- a/keep/api/utils/enrichment_helpers.py
+++ b/keep/api/utils/enrichment_helpers.py
@@ -108,9 +108,7 @@ def convert_db_alerts_to_dto_alerts(alerts: list[Alert]) -> list[AlertDto]:
)
continue
- # include the db event id if it's not present
- if alert_dto.event_id is None:
- alert_dto.event_id = str(alert.id)
+ alert_dto.event_id = str(alert.id)
# enrich provider id when it's possible
if alert_dto.providerId is None: