Skip to content

Commit

Permalink
feat: receive alerts from emails
Browse files Browse the repository at this point in the history
  • Loading branch information
talboren committed Oct 1, 2024
1 parent b3669f1 commit 086ed8c
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 49 deletions.
98 changes: 53 additions & 45 deletions keep-ui/app/alerts/alert-table-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import AlertExtraPayload from "./alert-extra-payload";
import AlertMenu from "./alert-menu";
import { isSameDay, isValid, isWithinInterval, startOfDay } from "date-fns";
import { severityMapping } from "./models";
import { MdOutlineNotificationsActive, MdOutlineNotificationsOff } from "react-icons/md";
import {
MdOutlineNotificationsActive,
MdOutlineNotificationsOff,
} from "react-icons/md";

export const DEFAULT_COLS = [
"noise",
Expand Down Expand Up @@ -82,12 +85,13 @@ export const isDateWithinRange: FilterFn<AlertDto> = (row, columnId, value) => {

const columnHelper = createColumnHelper<AlertDto>();

const invertedSeverityMapping = Object.entries(severityMapping).reduce<{ [key: string]: number }>((acc, [key, value]) => {
const invertedSeverityMapping = Object.entries(severityMapping).reduce<{
[key: string]: number;
}>((acc, [key, value]) => {
acc[value as keyof typeof acc] = Number(key);
return acc;
}, {});


const customSeveritySortFn = (rowA: any, rowB: any) => {
// Adjust the way to access severity values according to your data structure
const severityValueA = rowA.original?.severity; // or rowA.severity;
Expand Down Expand Up @@ -160,39 +164,38 @@ export const useAlertTableCols = (
) as ColumnDef<AlertDto>[];

return [
// noisy column
columnHelper.display({
id: "noise",
size: 5,
header: () => <></>,
cell: (context) => {
// Get the status of the alert
const status = context.row.original.status;
const isNoisy = context.row.original.isNoisy;
// noisy column
columnHelper.display({
id: "noise",
size: 5,
header: () => <></>,
cell: (context) => {
// Get the status of the alert
const status = context.row.original.status;
const isNoisy = context.row.original.isNoisy;

// Return null if presetNoisy is not true
if (!presetNoisy && !isNoisy) {
return null;
}
else if (presetNoisy) {
// Decide which icon to display based on the status
if (status === "firing") {
return <Icon icon={MdOutlineNotificationsActive} color="red" />;
} else {
return <Icon icon={MdOutlineNotificationsOff} color="red" />;
}
// Return null if presetNoisy is not true
if (!presetNoisy && !isNoisy) {
return null;
} else if (presetNoisy) {
// Decide which icon to display based on the status
if (status === "firing") {
return <Icon icon={MdOutlineNotificationsActive} color="red" />;
} else {
return <Icon icon={MdOutlineNotificationsOff} color="red" />;
}
// else, noisy alert in non noisy preset
else {
if (status === "firing") {
return <Icon icon={MdOutlineNotificationsActive} color="red" />;
} else {
return null;
}
}
// else, noisy alert in non noisy preset
else {
if (status === "firing") {
return <Icon icon={MdOutlineNotificationsActive} color="red" />;
} else {
return null;
}
},
enableSorting: false,
}),
}
},
enableSorting: false,
}),
,
...(isCheckboxDisplayed
? [
Expand Down Expand Up @@ -222,7 +225,6 @@ export const useAlertTableCols = (
minSize: 100,
cell: (context) => <AlertSeverity severity={context.getValue()} />,
sortingFn: customSeveritySortFn,

}),
columnHelper.display({
id: "name",
Expand Down Expand Up @@ -267,17 +269,23 @@ export const useAlertTableCols = (
header: "Source",
minSize: 100,
cell: (context) =>
(context.getValue() ?? []).map((source, index) => (
<Image
className={`inline-block ${index == 0 ? "" : "-ml-2"}`}
key={source}
alt={source}
height={24}
width={24}
title={source}
src={`/icons/${source}-icon.png`}
/>
)),
(context.getValue() ?? []).map((source, index) => {
let imagePath = `/icons/${source}-icon.png`;
if (source.includes("@")) {
imagePath = "/icons/mailgun-icon.png";
}
return (
<Image
className={`inline-block ${index == 0 ? "" : "-ml-2"}`}
key={source}
alt={source}
height={24}
width={24}
title={source}
src={imagePath}
/>
);
}),
}),
columnHelper.accessor("assignee", {
id: "assignee",
Expand Down
Binary file added keep-ui/public/icons/mailgun-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions keep/api/core/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import logging
import os

from fastapi import Request
from fastapi.datastructures import FormData
from pusher import Pusher

logger = logging.getLogger(__name__)
Expand All @@ -11,6 +13,25 @@
SINGLE_TENANT_EMAIL = "admin@keephq"


async def extract_generic_body(request: Request) -> dict | bytes | FormData:
"""
Extracts the body of the request based on the content type.
Args:
request (Request): The request object.
Returns:
dict | bytes | FormData: The body of the request.
"""
content_type = request.headers.get("Content-Type")
if content_type == "application/json":
return await request.json()
elif content_type == "application/x-www-form-urlencoded":
return await request.form()
else:
return await request.body()


def get_pusher_client() -> Pusher | None:
if os.environ.get("PUSHER_DISABLED", "false") == "true":
return None
Expand Down
18 changes: 15 additions & 3 deletions keep/api/routes/alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from keep.api.core.config import config
from keep.api.core.db import get_alert_audit as get_alert_audit_db
from keep.api.core.db import get_alerts_by_fingerprint, get_enrichment, get_last_alerts
from keep.api.core.dependencies import get_pusher_client
from keep.api.core.dependencies import extract_generic_body, get_pusher_client
from keep.api.core.elastic import ElasticClient
from keep.api.models.alert import (
AlertDto,
Expand Down Expand Up @@ -347,18 +347,30 @@ async def webhook_challenge():
)
async def receive_event(
provider_type: str,
event: dict | bytes,
bg_tasks: BackgroundTasks,
request: Request,
provider_id: str | None = None,
fingerprint: str | None = None,
event=Depends(extract_generic_body),
authenticated_entity: AuthenticatedEntity = Depends(
IdentityManagerFactory.get_auth_verifier(["write:alert"])
),
pusher_client: Pusher = Depends(get_pusher_client),
) -> dict[str, str]:
trace_id = request.state.trace_id
provider_class = ProvidersFactory.get_provider_class(provider_type)

provider_class = None
try:
provider_class = ProvidersFactory.get_provider_class(provider_type)
except ModuleNotFoundError:
raise HTTPException(
status_code=400, detail=f"Provider {provider_type} not found"
)
if not provider_class:
raise HTTPException(
status_code=400, detail=f"Provider {provider_type} not found"
)

# Parse the raw body
event = provider_class.parse_event_raw_body(event)

Expand Down
7 changes: 6 additions & 1 deletion keep/api/tasks/process_event_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

# third-parties
from arq import Retry
from fastapi.datastructures import FormData
from sqlmodel import Session

# internals
Expand Down Expand Up @@ -516,7 +517,11 @@ def process_event(
except Exception:
logger.exception("Failed to run pre-formatting extraction rules")

if provider_type is not None and isinstance(event, dict):
if (
provider_type is not None
and isinstance(event, dict)
or isinstance(event, FormData)
):
provider_class = ProvidersFactory.get_provider_class(provider_type)
event = provider_class.format_alert(
tenant_id=tenant_id,
Expand Down
Empty file.
89 changes: 89 additions & 0 deletions keep/providers/mailgun_provider/mailgun_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""
Simple Console Output Provider
"""

import dataclasses
import datetime
import typing

import pydantic
from fastapi.datastructures import FormData

from keep.api.models.alert import AlertDto
from keep.contextmanager.contextmanager import ContextManager
from keep.providers.base.base_provider import BaseProvider
from keep.providers.models.provider_config import ProviderConfig
from keep.providers.providers_factory import ProvidersFactory


@pydantic.dataclasses.dataclass
class MailgunProviderAuthConfig:

extraction: typing.Optional[dict[str, str]] = dataclasses.field(
default=lambda: {},
metadata={
"description": "Extraction Rules",
"type": "form",
},
)


class MailgunProvider(BaseProvider):
def __init__(
self, context_manager: ContextManager, provider_id: str, config: ProviderConfig
):
super().__init__(context_manager, provider_id, config)

def validate_config(self):
self.authentication_config = MailgunProviderAuthConfig(
**self.config.authentication
)

def setup_webhook(
self, tenant_id: str, keep_api_url: str, api_key: str, setup_alerts: bool = True
):
return super().setup_webhook(tenant_id, keep_api_url, api_key, setup_alerts)

@staticmethod
def _format_alert(event: FormData) -> AlertDto:
name = event["subject"]
source = event["from"]
message = event["stripped-text"]
timestamp = datetime.datetime.fromtimestamp(
float(event["timestamp"])
).isoformat()
severity = "info" # to extract
status = "firing" # to extract
return AlertDto(
name=name,
source=[source],
message=message,
description=message,
lastReceived=timestamp,
severity=severity,
status=status,
raw_email={**event},
)


if __name__ == "__main__":
# Output debug messages
import logging

logging.basicConfig(level=logging.DEBUG, handlers=[logging.StreamHandler()])
context_manager = ContextManager(
tenant_id="singletenant",
workflow_id="test",
)
# Initalize the provider and provider config
config = {
"description": "Console Output Provider",
"authentication": {},
}
provider = ProvidersFactory.get_provider(
context_manager,
provider_id="mock",
provider_type="console",
provider_config=config,
)
provider.notify(alert_message="Simple alert showing context with name: John Doe")

0 comments on commit 086ed8c

Please sign in to comment.