Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/snippets/providers/mailgun-snippet-autogenerated.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ This provider requires authentication.
- **sender**: Sender email address to validate (required: False, sensitive: False)
- **email_domain**: Custom email domain for receiving alerts (required: False, sensitive: False)
- **extraction**: Extraction Rules (required: False, sensitive: False)
- **skip_dmarc_reports**: Skip DMARC reports (required: False, sensitive: False)
- **skip_spf_reports**: Skip SPF reports (required: False, sensitive: False)
- **handle_emails_without_body**: Handle emails without body content (required: False, sensitive: False)


## In workflows
Expand Down
18 changes: 18 additions & 0 deletions keep-ui/entities/alerts/model/useAlerts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,30 @@ export const useAlerts = () => {
}
};

// Function to reprocess error alerts with updated provider code
// If alertId is provided, reprocesses that specific alert
// If no alertId is provided, reprocesses all error alerts
const reprocessErrorAlerts = async (alertId?: string) => {
if (!api.isReady()) return { success: false, message: "API not ready" };

try {
const payload = alertId ? { alert_id: alertId } : {};
const result = await api.post(`/alerts/event/error/reprocess`, payload);
await mutate(); // Refresh the data
return { success: true, ...result };
} catch (error) {
console.error("Failed to reprocess error alert(s):", error);
return { success: false, message: "Failed to reprocess" };
}
};

return {
data,
error,
isLoading,
mutate,
dismissErrorAlerts,
reprocessErrorAlerts,
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
Button,
} from "@tremor/react";
import { DynamicImageProviderIcon } from "@/components/ui/DynamicProviderIcon";
import { toast } from "react-toastify";

interface ErrorAlert {
id: string;
Expand All @@ -32,9 +33,10 @@ export const AlertErrorEventModal: React.FC<AlertErrorEventModalProps> = ({
onClose,
}) => {
const { useErrorAlerts } = useAlerts();
const { data: errorAlerts, dismissErrorAlerts } = useErrorAlerts();
const { data: errorAlerts, dismissErrorAlerts, reprocessErrorAlerts } = useErrorAlerts();
const [selectedAlertId, setSelectedAlertId] = useState<string>("");
const [isDismissing, setIsDismissing] = useState<boolean>(false);
const [isReprocessing, setIsReprocessing] = useState<boolean>(false);

// Set the first alert as selected when data loads or changes
React.useEffect(() => {
Expand Down Expand Up @@ -96,6 +98,61 @@ export const AlertErrorEventModal: React.FC<AlertErrorEventModalProps> = ({
}
};

const handleReprocessSelected = async () => {
if (selectedAlert) {
setIsReprocessing(true);
try {
const result = await reprocessErrorAlerts(selectedAlert.id);
if (result.success) {
toast.success(
`Reprocessed successfully! ${result.message || ""}`,
{ position: "top-right" }
);

// Handle navigation after successful reprocessing
if (errorAlerts?.length === 1) {
setSelectedAlertId("");
onClose();
} else if (parseInt(selectedAlertId, 10) === errorAlerts.length - 1) {
setSelectedAlertId((parseInt(selectedAlertId, 10) - 1).toString());
}
} else {
toast.error(`Reprocessing failed: ${result.message}`, {
position: "top-right",
});
}
} catch (error) {
console.error("Failed to reprocess alert:", error);
toast.error("Failed to reprocess alert", { position: "top-right" });
} finally {
setIsReprocessing(false);
}
}
};

const handleReprocessAll = async () => {
setIsReprocessing(true);
try {
const result = await reprocessErrorAlerts();
if (result.success) {
toast.success(
`Reprocessed ${result.successful || 0} alert(s) successfully!`,
{ position: "top-right" }
);
onClose();
} else {
toast.error(`Reprocessing failed: ${result.message}`, {
position: "top-right",
});
}
} catch (error) {
console.error("Failed to reprocess alerts:", error);
toast.error("Failed to reprocess alerts", { position: "top-right" });
} finally {
setIsReprocessing(false);
}
};

return (
<Modal
isOpen={isOpen}
Expand Down Expand Up @@ -153,11 +210,28 @@ export const AlertErrorEventModal: React.FC<AlertErrorEventModalProps> = ({
</Select>
</div>
<div className="flex space-x-2">
<Button
size="xs"
color="blue"
onClick={handleReprocessSelected}
disabled={isReprocessing || !selectedAlert || isDismissing}
>
{isReprocessing ? "Reprocessing..." : "Reprocess current alert"}
</Button>
<Button
size="xs"
color="blue"
variant="secondary"
onClick={handleReprocessAll}
disabled={isReprocessing || isDismissing}
>
{isReprocessing ? "Reprocessing..." : `Reprocess All (${errorAlerts.length})`}
</Button>
<Button
size="xs"
color="orange"
onClick={handleDismissSelected}
disabled={isDismissing || !selectedAlert}
disabled={isDismissing || !selectedAlert || isReprocessing}
>
{isDismissing ? "Dismissing..." : "Dismiss current alert"}
</Button>
Expand All @@ -166,7 +240,7 @@ export const AlertErrorEventModal: React.FC<AlertErrorEventModalProps> = ({
color="orange"
variant="secondary"
onClick={handleDismissAll}
disabled={isDismissing}
disabled={isDismissing || isReprocessing}
>
{isDismissing ? "Dismissing..." : "Dismiss All"}
</Button>
Expand Down
61 changes: 61 additions & 0 deletions keep/api/core/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -5855,6 +5855,67 @@ def get_error_alerts(tenant_id: str, limit: int = 100) -> List[AlertRaw]:
)


def get_error_alerts_to_reprocess(
tenant_id: str, alert_id: str | None = None
) -> List[AlertRaw]:
"""
Get error alerts to reprocess.

Args:
tenant_id: Tenant ID
alert_id: Optional specific alert ID to reprocess

Returns:
List of AlertRaw objects to reprocess
"""
with Session(engine) as session:
query = session.query(AlertRaw).filter(
AlertRaw.tenant_id == tenant_id,
AlertRaw.error == True,
AlertRaw.dismissed == False,
)

if alert_id:
if isinstance(alert_id, str):
alert_id_uuid = uuid.UUID(alert_id)
else:
alert_id_uuid = alert_id
query = query.filter(AlertRaw.id == alert_id_uuid)

return query.all()


def dismiss_error_alert_by_id(tenant_id: str, alert_id: str, dismissed_by: str | None = None) -> None:
"""
Dismiss a specific error alert after successful reprocessing.

Args:
tenant_id: Tenant ID
alert_id: Alert ID to dismiss
dismissed_by: Optional user who dismissed the alert
"""
with Session(engine) as session:
if isinstance(alert_id, str):
alert_id_uuid = uuid.UUID(alert_id)
else:
alert_id_uuid = alert_id

stmt = (
update(AlertRaw)
.where(
AlertRaw.id == alert_id_uuid,
AlertRaw.tenant_id == tenant_id,
)
.values(
dismissed=True,
dismissed_by=dismissed_by,
dismissed_at=datetime.now(tz=timezone.utc),
)
)
session.execute(stmt)
session.commit()


def dismiss_error_alerts(tenant_id: str, alert_id=None, dismissed_by=None) -> None:
with Session(engine) as session:
stmt = (
Expand Down
120 changes: 120 additions & 0 deletions keep/api/routes/alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from keep.api.core.cel_to_sql.sql_providers.base import CelToSqlException
from keep.api.core.config import config
from keep.api.core.db import dismiss_error_alerts as dismiss_error_alerts_db
from keep.api.core.db import dismiss_error_alert_by_id
from keep.api.core.db import enrich_alerts_with_incidents
from keep.api.core.db import get_alert_audit as get_alert_audit_db
from keep.api.core.db import (
Expand All @@ -39,6 +40,7 @@
get_enrichment,
)
from keep.api.core.db import get_error_alerts as get_error_alerts_db
from keep.api.core.db import get_error_alerts_to_reprocess
from keep.api.core.db import (
get_last_alerts,
get_last_alerts_by_fingerprints,
Expand Down Expand Up @@ -1459,3 +1461,121 @@ def dismiss_error_alerts(
)

return {"success": True, "message": "Successfully dismissed all alerts"}


@router.post(
"/event/error/reprocess",
description="Reprocess error alerts with updated provider code. If alert_id is provided, reprocesses that specific alert. If no alert_id is provided, reprocesses all error alerts.",
)
def reprocess_error_alerts(
request: DismissAlertRequest = None,
authenticated_entity: AuthenticatedEntity = Depends(
IdentityManagerFactory.get_auth_verifier(["write:alert"])
),
) -> dict:
"""
Reprocess failed events with current provider code.
If alert_id is provided, reprocesses that specific alert.
If no alert_id is provided, reprocesses all error alerts.
"""
tenant_id = authenticated_entity.tenant_id
alert_id = request.alert_id if request else None

logger.info(
"Reprocessing error alerts",
extra={
"tenant_id": tenant_id,
"alert_id": alert_id,
},
)

# Get error alerts to reprocess
error_alerts = get_error_alerts_to_reprocess(tenant_id, alert_id)

if not error_alerts:
logger.info(
"No error alerts found to reprocess",
extra={"tenant_id": tenant_id, "alert_id": alert_id},
)
return {"success": True, "message": "No error alerts found to reprocess", "successful": 0, "failed": 0, "total": 0}

successful = 0
failed = 0
failed_alerts = []

for error_alert in error_alerts:
try:
logger.info(
"Attempting to reprocess error alert",
extra={
"tenant_id": tenant_id,
"alert_id": str(error_alert.id),
"provider_type": error_alert.provider_type,
},
)

# Attempt to reprocess the event
process_event(
ctx={}, # No arq context for manual reprocessing
tenant_id=tenant_id,
provider_type=error_alert.provider_type,
provider_id=None,
fingerprint=None,
api_key_name=None,
trace_id=None,
event=error_alert.raw_alert,
notify_client=True,
)

# If successful, mark the error alert as dismissed
dismiss_error_alert_by_id(
tenant_id,
str(error_alert.id),
dismissed_by=authenticated_entity.email
)
successful += 1

logger.info(
"Successfully reprocessed error alert",
extra={
"tenant_id": tenant_id,
"alert_id": str(error_alert.id),
},
)

except Exception as e:
logger.error(
f"Failed to reprocess error alert: {e}",
extra={
"tenant_id": tenant_id,
"alert_id": str(error_alert.id),
"error": str(e),
},
)
failed += 1
failed_alerts.append(
{"alert_id": str(error_alert.id), "error": str(e)}
)

logger.info(
"Reprocessing completed",
extra={
"tenant_id": tenant_id,
"successful": successful,
"failed": failed,
"total": len(error_alerts),
},
)

response = {
"success": successful > 0,
"message": f"Reprocessed {successful} alert(s) successfully, {failed} failed",
"successful": successful,
"failed": failed,
"total": len(error_alerts),
}

if failed_alerts:
response["failed_alerts"] = failed_alerts

return response
Loading
Loading