diff --git a/keep-ui/app/(keep)/alerts/alert-assignee.tsx b/keep-ui/app/(keep)/alerts/alert-assignee.tsx index c3dc67627..525d00224 100644 --- a/keep-ui/app/(keep)/alerts/alert-assignee.tsx +++ b/keep-ui/app/(keep)/alerts/alert-assignee.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { NameInitialsAvatar } from "react-name-initials-avatar"; -import { useUsers } from "utils/hooks/useUsers"; +import { useUsers } from "@/entities/users/model/useUsers"; interface Props { assignee: string | undefined; diff --git a/keep-ui/app/(keep)/alerts/alert-severity-border.tsx b/keep-ui/app/(keep)/alerts/alert-severity-border.tsx index 34b0b6d3e..f38add3d4 100644 --- a/keep-ui/app/(keep)/alerts/alert-severity-border.tsx +++ b/keep-ui/app/(keep)/alerts/alert-severity-border.tsx @@ -1,27 +1,59 @@ import clsx from "clsx"; import { Severity } from "./models"; +const getSeverityBgClassName = (severity?: Severity) => { + switch (severity) { + case "critical": + return "bg-red-500"; + case "high": + case "error": + return "bg-orange-500"; + case "warning": + return "bg-yellow-500"; + case "info": + return "bg-blue-500"; + default: + return "bg-emerald-500"; + } +}; + +const getSeverityLabelClassName = (severity?: Severity) => { + switch (severity) { + case "critical": + return "bg-red-100"; + case "high": + case "error": + return "bg-orange-100"; + case "warning": + return "bg-yellow-100"; + case "info": + return "bg-blue-100"; + default: + return "bg-emerald-100"; + } +}; + +const getSeverityTextClassName = (severity?: Severity) => { + switch (severity) { + case "critical": + return "text-red-500"; + case "high": + case "error": + return "text-orange-500"; + case "warning": + return "text-amber-900"; + case "info": + return "text-blue-500"; + default: + return "text-emerald-500"; + } +}; + export function AlertSeverityBorder({ severity, }: { severity: Severity | undefined; }) { - const getSeverityBgClassName = (severity?: Severity) => { - switch (severity) { - case "critical": - return "bg-red-500"; - case "high": - case "error": - return "bg-orange-500"; - case "warning": - return "bg-yellow-500"; - case "info": - return "bg-blue-500"; - default: - return "bg-emerald-500"; - } - }; - return (
); } + +export function AlertSeverityBorderIcon({ severity }: { severity: Severity }) { + return ( +
+ ); +} + +export function AlertSeverityLabel({ severity }: { severity: Severity }) { + return ( + +
+ + {severity} + + + ); +} diff --git a/keep-ui/app/(keep)/alerts/alert-sidebar.tsx b/keep-ui/app/(keep)/alerts/alert-sidebar.tsx index bf985960e..41ae7b46c 100644 --- a/keep-ui/app/(keep)/alerts/alert-sidebar.tsx +++ b/keep-ui/app/(keep)/alerts/alert-sidebar.tsx @@ -2,12 +2,20 @@ import { Fragment } from "react"; import Image from "next/image"; import { Dialog, Transition } from "@headlessui/react"; import { AlertDto } from "./models"; -import { Button, Title, Card, Badge } from "@tremor/react"; +import { Button, Title, Badge } from "@tremor/react"; import { IoMdClose } from "react-icons/io"; import AlertTimeline from "./alert-timeline"; import { useAlerts } from "utils/hooks/useAlerts"; import { TopologyMap } from "../topology/ui/map"; import { TopologySearchProvider } from "@/app/(keep)/topology/TopologySearchContext"; +import { + AlertSeverityBorderIcon, + AlertSeverityLabel, +} from "./alert-severity-border"; +import { FieldHeader } from "@/shared/ui/FieldHeader"; +import { QuestionMarkCircleIcon } from "@heroicons/react/20/solid"; +import { Tooltip } from "@/shared/ui/Tooltip"; +import { Link } from "@/components/ui"; type AlertSidebarProps = { isOpen: boolean; @@ -57,11 +65,14 @@ const AlertSidebar = ({ isOpen, toggle, alert }: AlertSidebarProps) => { {/*Will add soon*/} {/**/} {/**/} - - Alert Details - - Beta - + + {alert?.severity && ( + + )} + {alert?.name ? alert.name : "Alert Details"}
@@ -72,43 +83,63 @@ const AlertSidebar = ({ isOpen, toggle, alert }: AlertSidebarProps) => {
{alert && (
- -
-

- Name: {alert.name} -

-

- Service: {alert.service} -

-

- Severity: {alert.severity} -

-

- {alert.source![0]} -

-

- Description: {alert.description} -

-

- Fingerprint: {alert.fingerprint} -

-
-
- - - +
+

+ Name + {alert.name} +

+

+ Service + + {alert.service} + +

+

+ Source + {alert.source![0]} +

+

+ Description + {alert.description} +

+

+ + Fingerprint + + Fingerprints are unique identifiers associated with + alert instances in Keep. Every provider declares the + fields fingerprints are calculated upon.{" "} + + Docs + + + } + className="z-50" + > + + + + {alert.fingerprint} +

+
+ Related Services = ({ if (a.label === "n/a") return 1; if (b.label === "n/a") return -1; const orderDiff = - getSeverityOrder(a.label) - getSeverityOrder(b.label); + getSeverityOrder(b.label) - getSeverityOrder(a.label); if (orderDiff !== 0) return orderDiff; return b.count - a.count; }); @@ -253,7 +249,7 @@ export const AlertFacets: React.FC = ({ showSkeleton={showSkeleton} /> diff --git a/keep-ui/app/(keep)/alerts/alert-table-facet-utils.tsx b/keep-ui/app/(keep)/alerts/alert-table-facet-utils.tsx index a26f8ab1d..51c5ff196 100644 --- a/keep-ui/app/(keep)/alerts/alert-table-facet-utils.tsx +++ b/keep-ui/app/(keep)/alerts/alert-table-facet-utils.tsx @@ -1,16 +1,5 @@ import { FacetFilters } from "./alert-table-facet-types"; -import { AlertDto, Severity } from "./models"; -import { - ChevronDownIcon, - ChevronRightIcon, - UserCircleIcon, - BellIcon, - ExclamationCircleIcon, - CheckCircleIcon, - CircleStackIcon, - BellSlashIcon, - FireIcon, -} from "@heroicons/react/24/outline"; +import { AlertDto } from "./models"; import { isQuickPresetRange } from "@/components/ui/DateRangePicker"; export const getFilteredAlertsForFacet = ( @@ -83,32 +72,6 @@ export const getFilteredAlertsForFacet = ( }); }; -export const getStatusIcon = (status: string) => { - switch (status.toLowerCase()) { - case "firing": - return ExclamationCircleIcon; - case "resolved": - return CheckCircleIcon; - case "acknowledged": - return CircleStackIcon; - default: - return CircleStackIcon; - } -}; - -export const getStatusColor = (status: string) => { - switch (status.toLowerCase()) { - case "firing": - return "red"; - case "resolved": - return "green"; - case "acknowledged": - return "blue"; - default: - return "gray"; - } -}; - export const getSeverityOrder = (severity: string): number => { switch (severity) { case "low": diff --git a/keep-ui/app/(keep)/alerts/alert-table-facet-value.tsx b/keep-ui/app/(keep)/alerts/alert-table-facet-value.tsx index 15bc1d991..83b588c03 100644 --- a/keep-ui/app/(keep)/alerts/alert-table-facet-value.tsx +++ b/keep-ui/app/(keep)/alerts/alert-table-facet-value.tsx @@ -1,17 +1,22 @@ -import React from "react"; +import React, { useCallback, useMemo } from "react"; import { Icon } from "@tremor/react"; import Image from "next/image"; import { Text } from "@tremor/react"; import { FacetValueProps } from "./alert-table-facet-types"; -import { getStatusIcon, getStatusColor } from "./alert-table-facet-utils"; -import { - UserCircleIcon, - BellIcon, - BellSlashIcon, - FireIcon, -} from "@heroicons/react/24/outline"; -import AlertSeverity from "./alert-severity"; +import { getStatusIcon, getStatusColor } from "@/shared/lib/status-utils"; +import { BellIcon, BellSlashIcon, FireIcon } from "@heroicons/react/24/outline"; import { Severity } from "./models"; +import { AlertSeverityBorderIcon } from "./alert-severity-border"; +import clsx from "clsx"; +import { useIncidents } from "@/utils/hooks/useIncidents"; +import { getIncidentName } from "@/entities/incidents/lib/utils"; +import { UserStatefulAvatar } from "@/entities/users/ui"; +import { useUser } from "@/entities/users/model/useUser"; + +const AssigneeLabel = ({ email }: { email: string }) => { + const user = useUser(email); + return user ? user.name : email; +}; export const FacetValue: React.FC = ({ label, @@ -22,6 +27,31 @@ export const FacetValue: React.FC = ({ showIcon = false, facetFilters, }) => { + const { data: incidents } = useIncidents( + true, + 100, + undefined, + undefined, + undefined, + { + revalidateOnFocus: false, + } + ); + + const incidentMap = useMemo(() => { + return new Map( + incidents?.items.map((incident) => [ + incident.id.replaceAll("-", ""), + incident, + ]) || [] + ); + }, [incidents]); + + const incident = useMemo( + () => (facetKey === "incident" ? incidentMap.get(label) : null), + [incidentMap, facetKey, label] + ); + const handleCheckboxClick = (e: React.MouseEvent) => { e.stopPropagation(); onSelect(label, false, false); @@ -41,6 +71,95 @@ export const FacetValue: React.FC = ({ } }; + const getValueIcon = useCallback( + (label: string, facetKey: string) => { + if (facetKey === "source") { + return ( + {label} + ); + } + if (facetKey === "severity") { + return ; + } + if (facetKey === "assignee") { + return ; + } + if (facetKey === "status") { + return ( + + ); + } + if (facetKey === "dismissed") { + return ( + + ); + } + if (facetKey === "incident") { + if (incident) { + return ( + + ); + } + return ( + + ); + } + return null; + }, + [incident] + ); + + const humanizeLabel = useCallback( + (label: string, facetKey: string) => { + if (facetKey === "assignee") { + if (label === "n/a") { + return "Not assigned"; + } + return ; + } + if (facetKey === "incident") { + if (label === "n/a") { + return "No incident"; + } + if (incident) { + return getIncidentName(incident); + } else { + return label; + } + } + if (facetKey === "dismissed") { + return label === "true" ? "Dismissed" : "Not dismissed"; + } + return {label}; + }, + [incident] + ); + const currentFilter = facetFilters[facetKey] || []; const isValueSelected = !currentFilter?.length || currentFilter.includes(label); @@ -61,54 +180,13 @@ export const FacetValue: React.FC = ({ />
-
+
{showIcon && ( -
- {facetKey === "source" && ( - {label} - )} - {facetKey === "severity" && ( - - )} - {facetKey === "assignee" && ( - - )} - {facetKey === "status" && ( - - )} - {facetKey === "dismissed" && ( - - )} - {facetKey === "incident" && ( - - )} +
+ {getValueIcon(label, facetKey)}
)} - {label} + {humanizeLabel(label, facetKey)}
diff --git a/keep-ui/app/(keep)/alerts/alert-table-headers.tsx b/keep-ui/app/(keep)/alerts/alert-table-headers.tsx index d1606ffc9..b70e563cd 100644 --- a/keep-ui/app/(keep)/alerts/alert-table-headers.tsx +++ b/keep-ui/app/(keep)/alerts/alert-table-headers.tsx @@ -63,8 +63,8 @@ const DraggableHeaderCell = ({ column.getIsPinned() !== false ? "default" : isDragging - ? "grabbing" - : "grab", + ? "grabbing" + : "grab", }; // TODO: fix multiple pinned columns @@ -82,8 +82,11 @@ const DraggableHeaderCell = ({ >
{/* Flex container */} + {children} {/* Column name or text */} {column.getCanSort() && ( // Sorting icon to the left <> + {/* Custom styled vertical line separator */} +
{ @@ -96,8 +99,8 @@ const DraggableHeaderCell = ({ column.getNextSortingOrder() === "asc" ? "Sort ascending" : column.getNextSortingOrder() === "desc" - ? "Sort descending" - : "Clear sort" + ? "Sort descending" + : "Clear sort" } > {/* Icon logic */} @@ -111,11 +114,8 @@ const DraggableHeaderCell = ({ )} - {/* Custom styled vertical line separator */} -
)} - {children} {/* Column name or text */}
{column.getIsPinned() === false && ( diff --git a/keep-ui/app/(keep)/alerts/alert-table-utils.tsx b/keep-ui/app/(keep)/alerts/alert-table-utils.tsx index bd41cbc22..0f5924559 100644 --- a/keep-ui/app/(keep)/alerts/alert-table-utils.tsx +++ b/keep-ui/app/(keep)/alerts/alert-table-utils.tsx @@ -22,6 +22,7 @@ import { MdOutlineNotificationsOff, } from "react-icons/md"; import { AlertSeverityBorder } from "./alert-severity-border"; +import { getStatusIcon, getStatusColor } from "@/shared/lib/status-utils"; export const DEFAULT_COLS = [ "severity", @@ -272,6 +273,17 @@ export const useAlertTableCols = ( id: "status", minSize: 100, header: "Status", + cell: (context) => ( + + + {context.getValue()} + + ), }), columnHelper.accessor("lastReceived", { id: "lastReceived", diff --git a/keep-ui/app/(keep)/alerts/alert-table.tsx b/keep-ui/app/(keep)/alerts/alert-table.tsx index 24b66ec7e..d61cb2167 100644 --- a/keep-ui/app/(keep)/alerts/alert-table.tsx +++ b/keep-ui/app/(keep)/alerts/alert-table.tsx @@ -1,8 +1,7 @@ import { useRef, useState } from "react"; -import { Table, Callout, Card } from "@tremor/react"; +import { Table, Card } from "@tremor/react"; import { AlertsTableBody } from "./alerts-table-body"; import { AlertDto } from "./models"; -import { CircleStackIcon } from "@heroicons/react/24/outline"; import { getCoreRowModel, useReactTable, diff --git a/keep-ui/app/(keep)/alerts/alert-timeline.tsx b/keep-ui/app/(keep)/alerts/alert-timeline.tsx index 02fe484e0..6653ea45e 100644 --- a/keep-ui/app/(keep)/alerts/alert-timeline.tsx +++ b/keep-ui/app/(keep)/alerts/alert-timeline.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Subtitle, Button } from "@tremor/react"; +import { Subtitle, Button, Card, Title } from "@tremor/react"; import { Chrono } from "react-chrono"; import Image from "next/image"; import { ArrowPathIcon } from "@heroicons/react/24/outline"; @@ -71,9 +71,9 @@ const AlertTimeline: React.FC = ({ )); return ( -
+
- Timeline + Timeline
- {isLoading ? ( -
-

Loading...

-
- ) : ( -
- ({ - title: formatTimestamp(entry.timestamp), - })) || [] - } - hideControls - disableToolbar - borderLessCards - slideShow={false} - mode="VERTICAL" - theme={{ - primary: "orange", - secondary: "rgb(255 247 237)", - titleColor: "orange", - titleColorActive: "orange", - }} - fontSizes={{ - title: ".75rem", - }} - cardWidth={400} - cardHeight="auto" - classNames={{ - card: "hidden", - cardMedia: "hidden", - cardSubTitle: "hidden", - cardText: "hidden", - cardTitle: "hidden", - title: "mb-3", - contentDetails: "w-full !m-0", - }} - > - {content} - -
- )} + + {isLoading ? ( +
+

Loading...

+
+ ) : ( +
+ ({ + title: formatTimestamp(entry.timestamp), + })) || [] + } + hideControls + disableToolbar + borderLessCards + slideShow={false} + mode="VERTICAL" + theme={{ + primary: "orange", + secondary: "rgb(255 247 237)", + titleColor: "orange", + titleColorActive: "orange", + }} + fontSizes={{ + title: ".75rem", + }} + cardWidth={400} + cardHeight="auto" + classNames={{ + card: "hidden", + cardMedia: "hidden", + cardSubTitle: "hidden", + cardText: "hidden", + cardTitle: "hidden", + title: "mb-3", + contentDetails: "w-full !m-0", + }} + > + {content} + +
+ )} +
); }; diff --git a/keep-ui/app/(keep)/incidents/[id]/activity/incident-activity.tsx b/keep-ui/app/(keep)/incidents/[id]/activity/incident-activity.tsx index 0c803d477..2249e6d1e 100644 --- a/keep-ui/app/(keep)/incidents/[id]/activity/incident-activity.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/activity/incident-activity.tsx @@ -2,7 +2,7 @@ import { AlertDto } from "@/app/(keep)/alerts/models"; import { IncidentDto } from "@/entities/incidents/model"; -import { useUsers } from "@/utils/hooks/useUsers"; +import { useUsers } from "@/entities/users/model/useUsers"; import Image from "next/image"; import UserAvatar from "@/components/navbar/UserAvatar"; import "./incident-activity.css"; 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 799afbc28..0da0da3ce 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 @@ -1,4 +1,4 @@ -import { Icon } from "@tremor/react"; +import { Button, Icon } from "@tremor/react"; import { AlertDto } from "@/app/(keep)/alerts/models"; import { useHydratedSession as useSession } from "@/shared/lib/hooks/useHydratedSession"; import { toast } from "react-toastify"; @@ -43,14 +43,18 @@ export default function IncidentAlertMenu({ incidentId, alert }: Props) { } return ( -
- +
); } diff --git a/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alerts.tsx b/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alerts.tsx index 7d3d828e8..7d3f52ef2 100644 --- a/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alerts.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alerts.tsx @@ -8,6 +8,7 @@ import { } from "@tanstack/react-table"; import { Card, + Icon, Table, TableBody, TableCell, @@ -33,6 +34,9 @@ import { getCommonPinningStylesAndClassNames } from "@/components/ui/table/utils import { EmptyStateCard } from "@/components/ui"; import { useRouter } from "next/navigation"; import { TablePagination } from "@/shared/ui"; +import { AlertSeverityBorder } from "@/app/(keep)/alerts/alert-severity-border"; +import { getStatusIcon } from "@/shared/lib/status-utils"; +import { getStatusColor } from "@/shared/lib/status-utils"; interface Props { incident: IncidentDto; @@ -110,15 +114,17 @@ export default function IncidentAlerts({ incident }: Props) { // /> // ), // }), - columnHelper.accessor("severity", { + columnHelper.display({ id: "severity", - header: "Severity", - minSize: 80, + maxSize: 4, + header: () => <>, cell: (context) => ( -
- -
+ ), + meta: { + tdClassName: "p-0", + thClassName: "p-0", + }, }), columnHelper.display({ id: "name", @@ -144,17 +150,28 @@ export default function IncidentAlerts({ incident }: Props) { id: "status", minSize: 100, header: "Status", + cell: (context) => ( + + + {context.getValue()} + + ), }), columnHelper.accessor("is_created_by_ai", { id: "is_created_by_ai", - header: "🔗", + header: "🔗 Correlation type", minSize: 50, cell: (context) => ( <> {context.getValue() ? ( -
🤖
+
🤖 AI
) : ( -
👨‍💻
+
👨‍💻 Manually
)} ), @@ -186,7 +203,7 @@ export default function IncidentAlerts({ incident }: Props) { }), columnHelper.display({ id: "remove", - header: "", + header: "Correlation", cell: (context) => incident.is_confirmed && ( { + const { providersSelectedCategories, setProvidersSelectedCategories } = + useFilterContext(); + + const categories: TProviderCategory[] = [ + "Monitoring", + "Incident Management", + "Cloud Infrastructure", + "Ticketing", + "Developer Tools", + "Database", + "Identity and Access Management", + "Security", + "Collaboration", + "CRM", + "Queues", + "Coming Soon", + "Others", + ]; + + const toggleCategory = (category: TProviderCategory) => { + setProvidersSelectedCategories((prev) => + prev.includes(category) + ? prev.filter((c) => c !== category) + : [...prev, category] + ); + }; + + return ( +
+ {categories.map((category) => ( + toggleCategory(category)} + > + {category} + + ))} +
+ ); +}; diff --git a/keep-ui/app/(keep)/providers/components/providers-filter-by-label/providers-filter-by-label.tsx b/keep-ui/app/(keep)/providers/components/providers-filter-by-label/providers-filter-by-label.tsx index 09f696c8e..669c966f1 100644 --- a/keep-ui/app/(keep)/providers/components/providers-filter-by-label/providers-filter-by-label.tsx +++ b/keep-ui/app/(keep)/providers/components/providers-filter-by-label/providers-filter-by-label.tsx @@ -18,8 +18,8 @@ export const ProvidersFilterByLabel: FC = (props) => { {options.map(([value, label]) => ( diff --git a/keep-ui/app/(keep)/providers/components/providers-search/providers-search.tsx b/keep-ui/app/(keep)/providers/components/providers-search/providers-search.tsx index 5d4965038..604f24bd5 100644 --- a/keep-ui/app/(keep)/providers/components/providers-search/providers-search.tsx +++ b/keep-ui/app/(keep)/providers/components/providers-search/providers-search.tsx @@ -16,6 +16,7 @@ export const ProvidersSearch: FC = () => { id="search-providers" icon={MagnifyingGlassIcon} placeholder="Filter providers..." + className="w-full" value={providersSearchString} onChange={handleChange} /> diff --git a/keep-ui/app/(keep)/providers/filter-context/filter-context.tsx b/keep-ui/app/(keep)/providers/filter-context/filter-context.tsx index 1e2866721..e61f91dba 100644 --- a/keep-ui/app/(keep)/providers/filter-context/filter-context.tsx +++ b/keep-ui/app/(keep)/providers/filter-context/filter-context.tsx @@ -2,7 +2,7 @@ import { createContext, useState, FC, PropsWithChildren } from "react"; import { IFilterContext } from "./types"; import { useSearchParams } from "next/navigation"; import { PROVIDER_LABELS_KEYS } from "./constants"; -import type { TProviderLabels } from "../providers"; +import type { TProviderCategory, TProviderLabels } from "../providers"; export const FilterContext = createContext(null); @@ -12,6 +12,9 @@ export const FilerContextProvider: FC = ({ children }) => { const [providersSearchString, setProvidersSearchString] = useState(""); + const [providersSelectedCategories, setProvidersSelectedCategories] = + useState([]); + const [providersSelectedTags, setProvidersSelectedTags] = useState< TProviderLabels[] >(() => { @@ -26,8 +29,10 @@ export const FilerContextProvider: FC = ({ children }) => { const contextValue: IFilterContext = { providersSearchString, providersSelectedTags, + providersSelectedCategories, setProvidersSelectedTags, setProvidersSearchString, + setProvidersSelectedCategories, }; return ( diff --git a/keep-ui/app/(keep)/providers/filter-context/types.ts b/keep-ui/app/(keep)/providers/filter-context/types.ts index c56f163b6..aeb6f0d5c 100644 --- a/keep-ui/app/(keep)/providers/filter-context/types.ts +++ b/keep-ui/app/(keep)/providers/filter-context/types.ts @@ -1,9 +1,11 @@ import { Dispatch, SetStateAction } from "react"; -import { TProviderLabels } from "../providers"; +import { TProviderCategory, TProviderLabels } from "../providers"; export interface IFilterContext { providersSearchString: string; providersSelectedTags: TProviderLabels[]; + providersSelectedCategories: TProviderCategory[]; setProvidersSearchString: Dispatch>; setProvidersSelectedTags: Dispatch>; + setProvidersSelectedCategories: Dispatch>; } diff --git a/keep-ui/app/(keep)/providers/layout.tsx b/keep-ui/app/(keep)/providers/layout.tsx index 83d2cc7fc..0c229eef2 100644 --- a/keep-ui/app/(keep)/providers/layout.tsx +++ b/keep-ui/app/(keep)/providers/layout.tsx @@ -3,16 +3,18 @@ import { PropsWithChildren } from "react"; import { ProvidersFilterByLabel } from "./components/providers-filter-by-label"; import { ProvidersSearch } from "./components/providers-search"; import { FilerContextProvider } from "./filter-context"; +import { ProvidersCategories } from "./components/providers-categories"; export default function ProvidersLayout({ children }: PropsWithChildren) { return (
-
-
+
+
+
{children}
diff --git a/keep-ui/app/(keep)/providers/page.client.tsx b/keep-ui/app/(keep)/providers/page.client.tsx index fd49f8ff9..b4e4131ad 100644 --- a/keep-ui/app/(keep)/providers/page.client.tsx +++ b/keep-ui/app/(keep)/providers/page.client.tsx @@ -110,7 +110,11 @@ export default function ProvidersPage({ session, isLocalhost, } = useFetchProviders(); - const { providersSearchString, providersSelectedTags } = useFilterContext(); + const { + providersSearchString, + providersSelectedTags, + providersSelectedCategories, + } = useFilterContext(); const apiUrl = useApiUrl(); const router = useRouter(); useEffect(() => { @@ -147,6 +151,21 @@ export default function ProvidersPage({ ); }; + const searchCategories = (provider: Provider) => { + if (providersSelectedCategories.includes("Coming Soon")) { + if (provider.coming_soon) { + return true; + } + } + + return ( + providersSelectedCategories.length === 0 || + provider.categories.some((category) => + providersSelectedCategories.includes(category) + ) + ); + }; + const searchTags = (provider: Provider) => { return ( providersSelectedTags.length === 0 || @@ -171,7 +190,10 @@ export default function ProvidersPage({ )} searchProviders(provider) && searchTags(provider) + (provider) => + searchProviders(provider) && + searchTags(provider) && + searchCategories(provider) )} isLocalhost={isLocalhost} /> diff --git a/keep-ui/app/(keep)/providers/provider-tile.tsx b/keep-ui/app/(keep)/providers/provider-tile.tsx index 34949538f..2e4ef9c95 100644 --- a/keep-ui/app/(keep)/providers/provider-tile.tsx +++ b/keep-ui/app/(keep)/providers/provider-tile.tsx @@ -159,16 +159,17 @@ export default function ProviderTile({ provider, onClick }: Props) { /> ); }; - return (
diff --git a/keep-ui/app/(keep)/settings/auth/groups-tab.tsx b/keep-ui/app/(keep)/settings/auth/groups-tab.tsx index da420f719..dcc23901b 100644 --- a/keep-ui/app/(keep)/settings/auth/groups-tab.tsx +++ b/keep-ui/app/(keep)/settings/auth/groups-tab.tsx @@ -15,7 +15,7 @@ import { } from "@tremor/react"; import Loading from "@/app/(keep)/loading"; import { useGroups } from "utils/hooks/useGroups"; -import { useUsers } from "utils/hooks/useUsers"; +import { useUsers } from "@/entities/users/model/useUsers"; import { useRoles } from "utils/hooks/useRoles"; import { useState, useEffect, useMemo } from "react"; import GroupsSidebar from "./groups-sidebar"; diff --git a/keep-ui/app/(keep)/settings/auth/permissions-tab.tsx b/keep-ui/app/(keep)/settings/auth/permissions-tab.tsx index 08f69e159..69b0c4d17 100644 --- a/keep-ui/app/(keep)/settings/auth/permissions-tab.tsx +++ b/keep-ui/app/(keep)/settings/auth/permissions-tab.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from "react"; import { Title, Subtitle, Card, TextInput } from "@tremor/react"; import { usePermissions } from "utils/hooks/usePermissions"; -import { useUsers } from "utils/hooks/useUsers"; +import { useUsers } from "@/entities/users/model/useUsers"; import { useGroups } from "utils/hooks/useGroups"; import { useRoles } from "utils/hooks/useRoles"; import { usePresets } from "utils/hooks/usePresets"; diff --git a/keep-ui/app/(keep)/settings/auth/users-settings.tsx b/keep-ui/app/(keep)/settings/auth/users-settings.tsx index 78d2952d2..54cfeb03d 100644 --- a/keep-ui/app/(keep)/settings/auth/users-settings.tsx +++ b/keep-ui/app/(keep)/settings/auth/users-settings.tsx @@ -4,7 +4,7 @@ import Loading from "@/app/(keep)/loading"; import { User as AuthUser } from "next-auth"; import { TiUserAdd } from "react-icons/ti"; import { AuthType } from "utils/authenticationType"; -import { useUsers } from "utils/hooks/useUsers"; +import { useUsers } from "@/entities/users/model/useUsers"; import { useRoles } from "utils/hooks/useRoles"; import { useGroups } from "utils/hooks/useGroups"; import { useConfig } from "utils/hooks/useConfig"; diff --git a/keep-ui/app/(signin)/layout.tsx b/keep-ui/app/(signin)/layout.tsx index 5ff842ef0..c535b8c48 100644 --- a/keep-ui/app/(signin)/layout.tsx +++ b/keep-ui/app/(signin)/layout.tsx @@ -1,6 +1,6 @@ export const metadata = { - title: "Next.js", - description: "Generated by Next.js", + title: "Keep", + description: "The open-source alert management and AIOps platform", }; export default function RootLayout({ diff --git a/keep-ui/components/navbar/UserAvatar.tsx b/keep-ui/components/navbar/UserAvatar.tsx index d90300363..c2ae3c996 100644 --- a/keep-ui/components/navbar/UserAvatar.tsx +++ b/keep-ui/components/navbar/UserAvatar.tsx @@ -1,8 +1,10 @@ +import clsx from "clsx"; import Image from "next/image"; interface Props { image: string | null | undefined; name: string; + size?: "sm" | "xs"; } export const getInitials = (name: string) => @@ -10,17 +12,30 @@ export const getInitials = (name: string) => .join("") .toUpperCase(); -export default function UserAvatar({ image, name }: Props) { +export default function UserAvatar({ image, name, size = "sm" }: Props) { + const sizeClass = (function (size: "sm" | "xs") { + if (size === "sm") return "w-7 h-7"; + if (size === "xs") return "w-5 h-5"; + })(size); + const sizeValue = (function (size: "sm" | "xs") { + if (size === "sm") return 28; + if (size === "xs") return 20; + })(size); return image ? ( user avatar ) : ( - + {getInitials(name)} diff --git a/keep-ui/entities/users/model/useUser.ts b/keep-ui/entities/users/model/useUser.ts new file mode 100644 index 000000000..f765314cd --- /dev/null +++ b/keep-ui/entities/users/model/useUser.ts @@ -0,0 +1,6 @@ +import { useUsers } from "./useUsers"; + +export function useUser(email: string) { + const { data: users = [] } = useUsers(); + return users.find((user) => user.email === email) ?? null; +} diff --git a/keep-ui/utils/hooks/useUsers.ts b/keep-ui/entities/users/model/useUsers.ts similarity index 73% rename from keep-ui/utils/hooks/useUsers.ts rename to keep-ui/entities/users/model/useUsers.ts index 48368b77c..d87bb235f 100644 --- a/keep-ui/utils/hooks/useUsers.ts +++ b/keep-ui/entities/users/model/useUsers.ts @@ -2,7 +2,7 @@ import { User } from "@/app/(keep)/settings/models"; import { useHydratedSession as useSession } from "@/shared/lib/hooks/useHydratedSession"; import { SWRConfiguration } from "swr"; import useSWRImmutable from "swr/immutable"; -import { useApiUrl } from "./useConfig"; +import { useApiUrl } from "../../../utils/hooks/useConfig"; import { fetcher } from "utils/fetcher"; export const useUsers = (options: SWRConfiguration = {}) => { @@ -10,8 +10,8 @@ export const useUsers = (options: SWRConfiguration = {}) => { const { data: session } = useSession(); return useSWRImmutable( - () => (session ? `${apiUrl}/auth/users` : null), - (url) => fetcher(url, session?.accessToken), + () => (session ? "/auth/users" : null), + (url) => fetcher(apiUrl + url, session?.accessToken), options ); }; diff --git a/keep-ui/entities/users/ui/UserStatefulAvatar.tsx b/keep-ui/entities/users/ui/UserStatefulAvatar.tsx new file mode 100644 index 000000000..53d9bf43d --- /dev/null +++ b/keep-ui/entities/users/ui/UserStatefulAvatar.tsx @@ -0,0 +1,28 @@ +import UserAvatar from "@/components/navbar/UserAvatar"; +import { useUser } from "../model/useUser"; +import { Icon } from "@tremor/react"; +import { UserCircleIcon } from "@heroicons/react/24/outline"; +import clsx from "clsx"; + +export function UserStatefulAvatar({ + email, + size = "sm", +}: { + email: string; + size?: "sm" | "xs"; +}) { + const user = useUser(email); + const sizeClass = (function (size: "sm" | "xs") { + if (size === "sm") return "[&>svg]:w-7 [&>svg]:h-7"; + if (size === "xs") return "[&>svg]:w-5 [&>svg]:h-5"; + })(size); + if (!user) { + return ( + + ); + } + return ; +} diff --git a/keep-ui/entities/users/ui/index.ts b/keep-ui/entities/users/ui/index.ts new file mode 100644 index 000000000..b419f1b44 --- /dev/null +++ b/keep-ui/entities/users/ui/index.ts @@ -0,0 +1 @@ +export { UserStatefulAvatar } from "./UserStatefulAvatar"; diff --git a/keep-ui/features/create-or-update-incident/ui/create-or-update-incident-form.tsx b/keep-ui/features/create-or-update-incident/ui/create-or-update-incident-form.tsx index ddf50f976..0458f6007 100644 --- a/keep-ui/features/create-or-update-incident/ui/create-or-update-incident-form.tsx +++ b/keep-ui/features/create-or-update-incident/ui/create-or-update-incident-form.tsx @@ -10,7 +10,7 @@ import { SelectItem, } from "@tremor/react"; import { FormEvent, useEffect, useState } from "react"; -import { useUsers } from "utils/hooks/useUsers"; +import { useUsers } from "@/entities/users/model/useUsers"; import { useIncidentActions } from "@/entities/incidents/model"; import type { IncidentDto } from "@/entities/incidents/model"; import { getIncidentName } from "@/entities/incidents/lib/utils"; diff --git a/keep-ui/features/incident-list/ui/incidents-table.tsx b/keep-ui/features/incident-list/ui/incidents-table.tsx index f71c702a5..7f34c5813 100644 --- a/keep-ui/features/incident-list/ui/incidents-table.tsx +++ b/keep-ui/features/incident-list/ui/incidents-table.tsx @@ -35,6 +35,7 @@ import { useIncidentActions } from "@/entities/incidents/model"; import { IncidentSeverityBadge } from "@/entities/incidents/ui"; import { getIncidentName } from "@/entities/incidents/lib/utils"; import { DateTimeField, TablePagination } from "@/shared/ui"; +import { UserStatefulAvatar } from "@/entities/users/ui"; function SelectedRowActions({ selectedRowIds, @@ -228,7 +229,9 @@ export default function IncidentsTable({ columnHelper.display({ id: "assignee", header: "Assignee", - cell: ({ row }) => row.original.assignee, + cell: ({ row }) => ( + + ), }), columnHelper.accessor("creation_time", { id: "creation_time", diff --git a/keep-ui/package-lock.json b/keep-ui/package-lock.json index 551791941..11e155b6a 100644 --- a/keep-ui/package-lock.json +++ b/keep-ui/package-lock.json @@ -24,6 +24,7 @@ "@heroicons/react": "^2.1.5", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-popover": "^1.1.2", + "@radix-ui/react-tooltip": "^1.1.4", "@sentry/nextjs": "^8.38.0", "@svgr/webpack": "^8.0.1", "@tanstack/react-table": "^8.11.0", @@ -6331,6 +6332,39 @@ } } }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.4.tgz", + "integrity": "sha512-QpObUH/ZlpaO4YgHSaYzrLO2VuO+ZBFFgGzjMUPwtiYnAzzNNDPJeEGRrT7qNOrWm/Jr08M1vlp+vTHtnSQ0Uw==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.0", + "@radix-ui/react-portal": "1.1.2", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", diff --git a/keep-ui/package.json b/keep-ui/package.json index 42a3e8395..172d2dfd2 100644 --- a/keep-ui/package.json +++ b/keep-ui/package.json @@ -25,6 +25,7 @@ "@heroicons/react": "^2.1.5", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-popover": "^1.1.2", + "@radix-ui/react-tooltip": "^1.1.4", "@sentry/nextjs": "^8.38.0", "@svgr/webpack": "^8.0.1", "@tanstack/react-table": "^8.11.0", diff --git a/keep-ui/public/icons/salesforce-icon.png b/keep-ui/public/icons/salesforce-icon.png new file mode 100644 index 000000000..25efdfea8 Binary files /dev/null and b/keep-ui/public/icons/salesforce-icon.png differ diff --git a/keep-ui/public/icons/zendesk-icon.png b/keep-ui/public/icons/zendesk-icon.png new file mode 100644 index 000000000..dd0ce0082 Binary files /dev/null and b/keep-ui/public/icons/zendesk-icon.png differ diff --git a/keep-ui/shared/lib/status-utils.ts b/keep-ui/shared/lib/status-utils.ts new file mode 100644 index 000000000..7eabd2f8f --- /dev/null +++ b/keep-ui/shared/lib/status-utils.ts @@ -0,0 +1,37 @@ +import { + ExclamationCircleIcon, + CheckCircleIcon, + CircleStackIcon, + PauseIcon, +} from "@heroicons/react/24/outline"; +import { IoIosGitPullRequest } from "react-icons/io"; + +export const getStatusIcon = (status: string) => { + switch (status.toLowerCase()) { + case "firing": + return ExclamationCircleIcon; + case "resolved": + return CheckCircleIcon; + case "acknowledged": + return PauseIcon; + case "merged": + return IoIosGitPullRequest; + default: + return CircleStackIcon; + } +}; + +export const getStatusColor = (status: string) => { + switch (status.toLowerCase()) { + case "firing": + return "red"; + case "resolved": + return "green"; + case "acknowledged": + return "gray"; + case "merged": + return "purple"; + default: + return "gray"; + } +}; diff --git a/keep-ui/shared/ui/FieldHeader.tsx b/keep-ui/shared/ui/FieldHeader.tsx index 5c99f4c23..c40813331 100644 --- a/keep-ui/shared/ui/FieldHeader.tsx +++ b/keep-ui/shared/ui/FieldHeader.tsx @@ -1,3 +1,13 @@ -export const FieldHeader = ({ children }: { children: React.ReactNode }) => ( -

{children}

+import clsx from "clsx"; + +export const FieldHeader = ({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) => ( +

+ {children} +

); diff --git a/keep-ui/shared/ui/Tooltip/Tooltip.tsx b/keep-ui/shared/ui/Tooltip/Tooltip.tsx new file mode 100644 index 000000000..7da4c271c --- /dev/null +++ b/keep-ui/shared/ui/Tooltip/Tooltip.tsx @@ -0,0 +1,93 @@ +// Tremor Tooltip [v0.1.0] + +import React from "react"; +import * as TooltipPrimitives from "@radix-ui/react-tooltip"; + +import clsx from "clsx"; + +interface TooltipProps + extends Omit, + Pick< + TooltipPrimitives.TooltipProps, + "open" | "defaultOpen" | "onOpenChange" | "delayDuration" + > { + content: React.ReactNode; + onClick?: React.MouseEventHandler; + side?: "bottom" | "left" | "top" | "right"; + showArrow?: boolean; +} + +const Tooltip = React.forwardRef< + React.ElementRef, + TooltipProps +>( + ( + { + children, + className, + content, + delayDuration, + defaultOpen, + open, + onClick, + onOpenChange, + showArrow = true, + side, + sideOffset = 10, + asChild, + ...props + }: TooltipProps, + forwardedRef + ) => { + return ( + + + + {children} + + + + {content} + {showArrow ? ( + + + + + ); + } +); + +Tooltip.displayName = "Tooltip"; + +export { Tooltip, type TooltipProps }; diff --git a/keep-ui/shared/ui/Tooltip/index.ts b/keep-ui/shared/ui/Tooltip/index.ts new file mode 100644 index 000000000..cf5cc016b --- /dev/null +++ b/keep-ui/shared/ui/Tooltip/index.ts @@ -0,0 +1,2 @@ +export { Tooltip } from "./Tooltip"; +export type { TooltipProps } from "./Tooltip"; diff --git a/keep-ui/shared/ui/index.ts b/keep-ui/shared/ui/index.ts index 307db1bbb..fc196b355 100644 --- a/keep-ui/shared/ui/index.ts +++ b/keep-ui/shared/ui/index.ts @@ -3,3 +3,6 @@ export { TabLinkNavigation, TabNavigationLink } from "./TabLinkNavigation"; export { DateTimeField } from "./DateTimeField"; export { FieldHeader } from "./FieldHeader"; export { EmptyStateCard } from "./EmptyState"; +export { Tooltip } from "./Tooltip"; + +export type { TooltipProps } from "./Tooltip"; diff --git a/keep-ui/tailwind.config.js b/keep-ui/tailwind.config.js index da4514760..9553ecaf2 100644 --- a/keep-ui/tailwind.config.js +++ b/keep-ui/tailwind.config.js @@ -105,11 +105,42 @@ module.exports = { "tremor-title": ["1.125rem", { lineHeight: "1.75rem" }], "tremor-metric": ["1.875rem", { lineHeight: "2.25rem" }], }, + keyframes: { + hide: { + from: { opacity: "1" }, + to: { opacity: "0" }, + }, + slideDownAndFade: { + from: { opacity: "0", transform: "translateY(-6px)" }, + to: { opacity: "1", transform: "translateY(0)" }, + }, + slideLeftAndFade: { + from: { opacity: "0", transform: "translateX(6px)" }, + to: { opacity: "1", transform: "translateX(0)" }, + }, + slideUpAndFade: { + from: { opacity: "0", transform: "translateY(6px)" }, + to: { opacity: "1", transform: "translateY(0)" }, + }, + slideRightAndFade: { + from: { opacity: "0", transform: "translateX(-6px)" }, + to: { opacity: "1", transform: "translateX(0)" }, + }, + }, animation: { "scroll-shadow-left": "auto linear 0s 1 normal none running scroll-shadow-left", "scroll-shadow-right": "auto linear 0s 1 normal none running scroll-shadow-right", + // Tremor tooltip + hide: "hide 150ms cubic-bezier(0.16, 1, 0.3, 1)", + slideDownAndFade: + "slideDownAndFade 150ms cubic-bezier(0.16, 1, 0.3, 1)", + slideLeftAndFade: + "slideLeftAndFade 150ms cubic-bezier(0.16, 1, 0.3, 1)", + slideUpAndFade: "slideUpAndFade 150ms cubic-bezier(0.16, 1, 0.3, 1)", + slideRightAndFade: + "slideRightAndFade 150ms cubic-bezier(0.16, 1, 0.3, 1)", }, }, }, diff --git a/keep/api/models/provider.py b/keep/api/models/provider.py index 1b3faebd1..7fbc4e3f3 100644 --- a/keep/api/models/provider.py +++ b/keep/api/models/provider.py @@ -43,8 +43,12 @@ class Provider(BaseModel): last_pull_time: datetime | None = None docs: str | None = None tags: list[ - Literal["alert", "ticketing", "messaging", "data", "queue", "topology", "incident"] + Literal[ + "alert", "ticketing", "messaging", "data", "queue", "topology", "incident" + ] ] = [] + categories: list[str] = ["Others"] + coming_soon: bool = False alertsDistribution: dict[str, int] | None = None alertExample: dict | None = None default_fingerprint_fields: list[str] | None = None diff --git a/keep/providers/aks_provider/aks_provider.py b/keep/providers/aks_provider/aks_provider.py index 937bbf591..c0199977b 100644 --- a/keep/providers/aks_provider/aks_provider.py +++ b/keep/providers/aks_provider/aks_provider.py @@ -72,6 +72,7 @@ class AksProvider(BaseProvider): """Enrich alerts using data from AKS.""" PROVIDER_DISPLAY_NAME = "Azure AKS" + PROVIDER_CATEGORY = ["Cloud Infrastructure"] def __init__( self, context_manager: ContextManager, provider_id: str, config: ProviderConfig diff --git a/keep/providers/appdynamics_provider/appdynamics_provider.py b/keep/providers/appdynamics_provider/appdynamics_provider.py index 2abd4bae2..15fd3b300 100644 --- a/keep/providers/appdynamics_provider/appdynamics_provider.py +++ b/keep/providers/appdynamics_provider/appdynamics_provider.py @@ -29,6 +29,7 @@ class AppdynamicsProviderAuthConfig: """ AppDynamics authentication configuration. """ + appDynamicsAccountName: str = dataclasses.field( metadata={ "required": True, @@ -87,10 +88,12 @@ def check_password_or_token(cls, values): username, password, token = ( values.get("appDynamicsUsername"), values.get("appDynamicsPassword"), - values.get("appDynamicsAccessToken") + values.get("appDynamicsAccessToken"), ) if not (username and password) and not token: - raise ValueError("Either username/password or access token must be provided") + raise ValueError( + "Either username/password or access token must be provided" + ) return values @@ -98,7 +101,7 @@ class AppdynamicsProvider(BaseProvider): """Install Webhooks and receive alerts from AppDynamics.""" PROVIDER_DISPLAY_NAME = "AppDynamics" - + PROVIDER_CATEGORY = ["Monitoring"] PROVIDER_SCOPES = [ ProviderScope( name="authenticated", @@ -196,7 +199,9 @@ def validate_scopes(self) -> dict[str, bool | str]: administrator = "Missing Administrator Privileges" self.logger.info("Validating AppDynamics Scopes") - user_id = self.get_user_id_by_name(self.authentication_config.appDynamicsAccountName) + user_id = self.get_user_id_by_name( + self.authentication_config.appDynamicsAccountName + ) url = self.__get_url( paths=[ @@ -237,13 +242,15 @@ def __get_headers(self): } def __get_auth(self) -> tuple[str, str]: - if self.authentication_config.appDynamicsUsername and self.authentication_config.appDynamicsPassword: + if ( + self.authentication_config.appDynamicsUsername + and self.authentication_config.appDynamicsPassword + ): return ( f"{self.authentication_config.appDynamicsUsername}@{self.authentication_config.appDynamicsAccountName}", self.authentication_config.appDynamicsPassword, ) - def __create_http_response_template(self, keep_api_url: str, api_key: str): keep_api_host, keep_api_path = keep_api_url.rsplit("/", 1) diff --git a/keep/providers/auth0_provider/auth0_provider.py b/keep/providers/auth0_provider/auth0_provider.py index e61598ad0..8e29289fb 100644 --- a/keep/providers/auth0_provider/auth0_provider.py +++ b/keep/providers/auth0_provider/auth0_provider.py @@ -1,6 +1,7 @@ """ Auth0 provider. """ + import dataclasses import datetime import os @@ -40,6 +41,7 @@ class Auth0Provider(BaseProvider): """Enrich alerts with data from Auth0.""" PROVIDER_DISPLAY_NAME = "Auth0" + PROVIDER_CATEGORY = ["Identity and Access Management"] provider_id: str config: ProviderConfig @@ -86,9 +88,9 @@ def _query(self, log_type: str, from_: str = None, **kwargs: dict): "per_page": 100, # specify the number of entries per page } if from_: - params[ - "q" - ] = f"({params['q']}) AND (date:[{from_} TO {datetime.datetime.now().isoformat()}])" + params["q"] = ( + f"({params['q']}) AND (date:[{from_} TO {datetime.datetime.now().isoformat()}])" + ) response = requests.get(url, headers=headers, params=params) response.raise_for_status() logs = response.json() diff --git a/keep/providers/axiom_provider/axiom_provider.py b/keep/providers/axiom_provider/axiom_provider.py index e60e42494..632dfddb7 100644 --- a/keep/providers/axiom_provider/axiom_provider.py +++ b/keep/providers/axiom_provider/axiom_provider.py @@ -1,6 +1,7 @@ """ AxiomProvider is a class that allows to ingest/digest data from Axiom. """ + import dataclasses from typing import Optional @@ -36,6 +37,7 @@ class AxiomProvider(BaseProvider): """Enrich alerts with data from Axiom.""" PROVIDER_DISPLAY_NAME = "Axiom" + PROVIDER_CATEGORY = ["Monitoring"] def __init__( self, context_manager: ContextManager, provider_id: str, config: ProviderConfig diff --git a/keep/providers/azuremonitoring_provider/azuremonitoring_provider.py b/keep/providers/azuremonitoring_provider/azuremonitoring_provider.py index 43793ae3c..5669af33a 100644 --- a/keep/providers/azuremonitoring_provider/azuremonitoring_provider.py +++ b/keep/providers/azuremonitoring_provider/azuremonitoring_provider.py @@ -47,6 +47,7 @@ class AzuremonitoringProvider(BaseProvider): } PROVIDER_DISPLAY_NAME = "Azure Monitor" + PROVIDER_CATEGORY = ["Monitoring", "Cloud Infrastructure"] def __init__( self, context_manager: ContextManager, provider_id: str, config: ProviderConfig diff --git a/keep/providers/base/base_provider.py b/keep/providers/base/base_provider.py index 493c29001..cea8ca82b 100644 --- a/keep/providers/base/base_provider.py +++ b/keep/providers/base/base_provider.py @@ -40,6 +40,27 @@ class BaseProvider(metaclass=abc.ABCMeta): PROVIDER_SCOPES: list[ProviderScope] = [] PROVIDER_METHODS: list[ProviderMethod] = [] FINGERPRINT_FIELDS: list[str] = [] + PROVIDER_COMING_SOON = False # tb: if the provider is coming soon, we show it in the UI but don't allow it to be added + PROVIDER_CATEGORY: list[ + Literal[ + "Monitoring", + "Incident Management", + "Cloud Infrastructure", + "Ticketing", + "Identity", + "Developer Tools", + "Database", + "Identity and Access Management", + "Security", + "Collaboration", + "Organizational Tools", + "CRM", + "Queues", + "Others", + ] + ] = [ + "Others" + ] # tb: Default category for providers that don't declare a category PROVIDER_TAGS: list[ Literal["alert", "ticketing", "messaging", "data", "queue", "topology"] ] = [] diff --git a/keep/providers/bigquery_provider/bigquery_provider.py b/keep/providers/bigquery_provider/bigquery_provider.py index 45e186963..8aabe4b8a 100644 --- a/keep/providers/bigquery_provider/bigquery_provider.py +++ b/keep/providers/bigquery_provider/bigquery_provider.py @@ -1,6 +1,7 @@ """ BigQuery provider. """ + import dataclasses import os from typing import Optional @@ -46,6 +47,7 @@ class BigqueryProvider(BaseProvider): config: ProviderConfig PROVIDER_DISPLAY_NAME = "BigQuery" + PROVIDER_CATEGORY = ["Cloud Infrastructure", "Database"] def __init__( self, context_manager: ContextManager, provider_id: str, config: ProviderConfig diff --git a/keep/providers/centreon_provider/centreon_provider.py b/keep/providers/centreon_provider/centreon_provider.py index 7d2f3e4fe..4bb5a343d 100644 --- a/keep/providers/centreon_provider/centreon_provider.py +++ b/keep/providers/centreon_provider/centreon_provider.py @@ -3,217 +3,234 @@ """ import dataclasses +import datetime import pydantic import requests -import datetime -from keep.api.models.alert import AlertDto, AlertStatus, AlertSeverity -from keep.exceptions.provider_exception import ProviderException +from keep.api.models.alert import AlertDto, AlertSeverity, AlertStatus from keep.contextmanager.contextmanager import ContextManager +from keep.exceptions.provider_exception import ProviderException from keep.providers.base.base_provider import BaseProvider from keep.providers.models.provider_config import ProviderConfig, ProviderScope + @pydantic.dataclasses.dataclass class CentreonProviderAuthConfig: - """ - CentreonProviderAuthConfig is a class that holds the authentication information for the CentreonProvider. - """ + """ + CentreonProviderAuthConfig is a class that holds the authentication information for the CentreonProvider. + """ + + host_url: str = dataclasses.field( + metadata={ + "required": True, + "description": "Centreon Host URL", + "sensitive": False, + }, + default=None, + ) + + api_token: str = dataclasses.field( + metadata={ + "required": True, + "description": "Centreon API Token", + "sensitive": True, + }, + default=None, + ) - host_url: str = dataclasses.field( - metadata={ - "required": True, - "description": "Centreon Host URL", - "sensitive": False, - }, - default=None, - ) - - api_token: str = dataclasses.field( - metadata={ - "required": True, - "description": "Centreon API Token", - "sensitive": True, - }, - default=None, - ) class CentreonProvider(BaseProvider): - PROVIDER_DISPLAY_NAME = "Centreon" - PROVIDER_TAGS = ["alert"] + PROVIDER_DISPLAY_NAME = "Centreon" + PROVIDER_TAGS = ["alert"] + PROVIDER_CATEGORY = ["Monitoring"] - PROVIDER_SCOPES = [ - ProviderScope( - name="authenticated", - description="User is authenticated" - ), - ] + PROVIDER_SCOPES = [ + ProviderScope(name="authenticated", description="User is authenticated"), + ] - """ + """ Centreon only supports the following host state (UP = 0, DOWN = 2, UNREA = 3) https://docs.centreon.com/docs/api/rest-api-v1/#realtime-information """ - STATUS_MAP = { - 2: AlertStatus.FIRING, - 3: AlertStatus.FIRING, - 0: AlertStatus.RESOLVED, - } - - SEVERITY_MAP = { - "CRITICAL": AlertSeverity.CRITICAL, - "WARNING": AlertSeverity.WARNING, - "UNKNOWN": AlertSeverity.INFO, - "OK": AlertSeverity.LOW, - "PENDING": AlertSeverity.INFO, - } - - def __init__( - self, context_manager: ContextManager, provider_id: str,config: ProviderConfig + STATUS_MAP = { + 2: AlertStatus.FIRING, + 3: AlertStatus.FIRING, + 0: AlertStatus.RESOLVED, + } + + SEVERITY_MAP = { + "CRITICAL": AlertSeverity.CRITICAL, + "WARNING": AlertSeverity.WARNING, + "UNKNOWN": AlertSeverity.INFO, + "OK": AlertSeverity.LOW, + "PENDING": AlertSeverity.INFO, + } + + def __init__( + self, context_manager: ContextManager, provider_id: str, config: ProviderConfig ): - super().__init__(context_manager, provider_id, config) + super().__init__(context_manager, provider_id, config) + + def dispose(self): + pass + + def validate_config(self): + """ + Validates the configuration of the Centreon provider. + """ + self.authentication_config = CentreonProviderAuthConfig( + **self.config.authentication + ) + + def __get_url(self, params: str): + url = self.authentication_config.host_url + "/centreon/api/index.php?" + params + return url + + def __get_headers(self): + return { + "Content-Type": "application/json", + "centreon-auth-token": self.authentication_config.api_token, + } + + def validate_scopes(self) -> dict[str, bool | str]: + """ + Validate the scopes of the provider. + """ + try: + response = requests.get( + self.__get_url("object=centreon_realtime_hosts&action=list"), + headers=self.__get_headers(), + ) + if response.ok: + scopes = {"authenticated": True} + else: + scopes = { + "authenticated": f"Error validating scopes: {response.status_code} {response.text}" + } + except Exception as e: + scopes = { + "authenticated": f"Error validating scopes: {e}", + } + + return scopes + + def __get_host_status(self) -> list[AlertDto]: + try: + url = self.__get_url("object=centreon_realtime_hosts&action=list") + response = requests.get(url, headers=self.__get_headers()) + + if not response.ok: + self.logger.error( + "Failed to get host status from Centreon: %s", response.json() + ) + raise ProviderException("Failed to get host status from Centreon") + + return [ + AlertDto( + id=host["id"], + name=host["name"], + address=host["address"], + description=host["output"], + status=host["state"], + severity=host["output"].split()[0], + instance_name=host["instance_name"], + acknowledged=host["acknowledged"], + max_check_attempts=host["max_check_attempts"], + lastReceived=datetime.datetime.fromtimestamp( + host["last_check"] + ).isoformat(), + source=["centreon"], + ) + for host in response.json() + ] + + except Exception as e: + self.logger.error("Error getting host status from Centreon: %s", e) + raise ProviderException(f"Error getting host status from Centreon: {e}") + + def __get_service_status(self) -> list[AlertDto]: + try: + url = self.__get_url("object=centreon_realtime_services&action=list") + response = requests.get(url, headers=self.__get_headers()) + + if not response.ok: + self.logger.error( + "Failed to get service status from Centreon: %s", response.json() + ) + raise ProviderException("Failed to get service status from Centreon") + + return [ + AlertDto( + id=service["service_id"], + host_id=service["host_id"], + name=service["name"], + description=service["description"], + status=service["state"], + severity=service["output"].split(":")[0], + acknowledged=service["acknowledged"], + max_check_attempts=service["max_check_attempts"], + lastReceived=datetime.datetime.fromtimestamp( + service["last_check"] + ).isoformat(), + source=["centreon"], + ) + for service in response.json() + ] + + except Exception as e: + self.logger.error("Error getting service status from Centreon: %s", e) + raise ProviderException(f"Error getting service status from Centreon: {e}") + + def _get_alerts(self) -> list[AlertDto]: + alerts = [] + try: + self.logger.info("Collecting alerts (host status) from Centreon") + host_status_alerts = self.__get_host_status() + alerts.extend(host_status_alerts) + except Exception as e: + self.logger.error("Error getting host status from Centreon: %s", e) + + try: + self.logger.info("Collecting alerts (service status) from Centreon") + service_status_alerts = self.__get_service_status() + alerts.extend(service_status_alerts) + except Exception as e: + self.logger.error("Error getting service status from Centreon: %s", e) + + return alerts - def dispose(self): - pass - def validate_config(self): - """ - Validates the configuration of the Centreon provider. - """ - self.authentication_config = CentreonProviderAuthConfig(**self.config.authentication) - - def __get_url(self, params: str): - url = self.authentication_config.host_url + "/centreon/api/index.php?" + params - return url - - def __get_headers(self): - return { - "Content-Type": "application/json", - "centreon-auth-token": self.authentication_config.api_token, - } - - def validate_scopes(self) -> dict[str, bool | str]: - """ - Validate the scopes of the provider. - """ - try: - response = requests.get(self.__get_url("object=centreon_realtime_hosts&action=list"), headers=self.__get_headers()) - if response.ok: - scopes = { - "authenticated": True - } - else: - scopes = { - "authenticated": f"Error validating scopes: {response.status_code} {response.text}" - } - except Exception as e: - scopes = { - "authenticated": f"Error validating scopes: {e}", - } - - return scopes - - def __get_host_status(self) -> list[AlertDto]: - try: - url = self.__get_url("object=centreon_realtime_hosts&action=list") - response = requests.get(url, headers=self.__get_headers()) - - if not response.ok: - self.logger.error("Failed to get host status from Centreon: %s", response.json()) - raise ProviderException("Failed to get host status from Centreon") - - return [AlertDto( - id=host["id"], - name=host["name"], - address=host["address"], - description=host["output"], - status=host["state"], - severity=host["output"].split()[0], - instance_name=host["instance_name"], - acknowledged=host["acknowledged"], - max_check_attempts=host["max_check_attempts"], - lastReceived=datetime.datetime.fromtimestamp(host["last_check"]).isoformat(), - source=["centreon"] - ) for host in response.json()] - - except Exception as e: - self.logger.error("Error getting host status from Centreon: %s", e) - raise ProviderException(f"Error getting host status from Centreon: {e}") - - def __get_service_status(self) -> list[AlertDto]: - try: - url = self.__get_url("object=centreon_realtime_services&action=list") - response = requests.get(url, headers=self.__get_headers()) - - if not response.ok: - self.logger.error("Failed to get service status from Centreon: %s", response.json()) - raise ProviderException("Failed to get service status from Centreon") - - return [AlertDto( - id=service["service_id"], - host_id=service["host_id"], - name=service["name"], - description=service["description"], - status=service["state"], - severity=service["output"].split(":")[0], - acknowledged=service["acknowledged"], - max_check_attempts=service["max_check_attempts"], - lastReceived=datetime.datetime.fromtimestamp(service["last_check"]).isoformat(), - source=["centreon"] - ) for service in response.json()] - - except Exception as e: - self.logger.error("Error getting service status from Centreon: %s", e) - raise ProviderException(f"Error getting service status from Centreon: {e}") - - def _get_alerts(self) -> list[AlertDto]: - alerts = [] - try: - self.logger.info("Collecting alerts (host status) from Centreon") - host_status_alerts = self.__get_host_status() - alerts.extend(host_status_alerts) - except Exception as e: - self.logger.error("Error getting host status from Centreon: %s", e) - - try: - self.logger.info("Collecting alerts (service status) from Centreon") - service_status_alerts = self.__get_service_status() - alerts.extend(service_status_alerts) - except Exception as e: - self.logger.error("Error getting service status from Centreon: %s", e) - - return alerts - if __name__ == "__main__": - import logging - - logging.basicConfig(level=logging.DEBUG, handlers=[logging.StreamHandler()]) - context_manager = ContextManager( - tenant_id="singletenant", - workflow_id="test", - ) - - import os - - host_url = os.environ.get("CENTREON_HOST_URL") - api_token = os.environ.get("CENTREON_API_TOKEN") - - if host_url is None: - raise ProviderException("CENTREON_HOST_URL is not set") - - config = ProviderConfig( - description="Centreon Provider", - authentication={ - "host_url": host_url, - "api_token": api_token, - }, - ) - - provider = CentreonProvider( - context_manager, - provider_id="centreon", - config=config, - ) - - provider._get_alerts() + import logging + + logging.basicConfig(level=logging.DEBUG, handlers=[logging.StreamHandler()]) + context_manager = ContextManager( + tenant_id="singletenant", + workflow_id="test", + ) + + import os + + host_url = os.environ.get("CENTREON_HOST_URL") + api_token = os.environ.get("CENTREON_API_TOKEN") + + if host_url is None: + raise ProviderException("CENTREON_HOST_URL is not set") + + config = ProviderConfig( + description="Centreon Provider", + authentication={ + "host_url": host_url, + "api_token": api_token, + }, + ) + + provider = CentreonProvider( + context_manager, + provider_id="centreon", + config=config, + ) + provider._get_alerts() diff --git a/keep/providers/checkmk_provider/checkmk_provider.py b/keep/providers/checkmk_provider/checkmk_provider.py index c9aa19be9..14dd763bd 100644 --- a/keep/providers/checkmk_provider/checkmk_provider.py +++ b/keep/providers/checkmk_provider/checkmk_provider.py @@ -2,17 +2,18 @@ Checkmk is a monitoring tool for Infrastructure and Application Monitoring. """ -from keep.api.models.alert import AlertDto, AlertStatus, AlertSeverity +from keep.api.models.alert import AlertDto, AlertSeverity, AlertStatus from keep.contextmanager.contextmanager import ContextManager from keep.providers.base.base_provider import BaseProvider from keep.providers.models.provider_config import ProviderConfig + class CheckmkProvider(BaseProvider): - """Get alerts from Checkmk into Keep""" + """Get alerts from Checkmk into Keep""" - webhook_description = "" - webhook_template = "" - webhook_markdown = """ + webhook_description = "" + webhook_template = "" + webhook_markdown = """ 💡 For more details on how to configure Checkmk to send alerts to Keep, see the [Keep documentation](https://docs.keephq.dev/providers/documentation/checkmk-provider). 1. Checkmk supports custom notification scripts. 2. Install Keep webhook script following the [Keep documentation](https://docs.keephq.dev/providers/documentation/checkmk-provider). @@ -26,84 +27,93 @@ class CheckmkProvider(BaseProvider): 10. Now Checkmk will be able to send alerts to Keep. """ - SEVERITIES_MAP = { - "OK": AlertSeverity.INFO, - "WARN": AlertSeverity.WARNING, - "CRIT": AlertSeverity.CRITICAL, - "UNKNOWN": AlertSeverity.INFO, - } - - STATUS_MAP = { - "UP": AlertStatus.RESOLVED, - "DOWN": AlertStatus.FIRING, - "UNREACH": AlertStatus.FIRING - } - - PROVIDER_DISPLAY_NAME = "Checkmk" - PROVIDER_TAGS = ["alert"] - - FINGERPRINT_FIELDS = ["id"] - - def __init__( - self, context_manager: ContextManager, provider_id: str, config: ProviderConfig - ): - super().__init__(context_manager, provider_id, config) - - def validate_config(): - """ - No validation required for Checkmk provider. - """ - pass + SEVERITIES_MAP = { + "OK": AlertSeverity.INFO, + "WARN": AlertSeverity.WARNING, + "CRIT": AlertSeverity.CRITICAL, + "UNKNOWN": AlertSeverity.INFO, + } - @staticmethod - def _format_alert(event: dict, provider_instance: BaseProvider = None) -> AlertDto | list[AlertDto]: - """ - Service alerts and Host alerts have different fields, so we are mapping the fields based on the event type. - """ - def _check_values(value): - if value not in event or event.get(value) == '': - return None - return event.get(value) - - """ + STATUS_MAP = { + "UP": AlertStatus.RESOLVED, + "DOWN": AlertStatus.FIRING, + "UNREACH": AlertStatus.FIRING, + } + + PROVIDER_DISPLAY_NAME = "Checkmk" + PROVIDER_TAGS = ["alert"] + PROVIDER_CATEGORY = ["Monitoring"] + FINGERPRINT_FIELDS = ["id"] + + def __init__( + self, context_manager: ContextManager, provider_id: str, config: ProviderConfig + ): + super().__init__(context_manager, provider_id, config) + + def validate_config(): + """ + No validation required for Checkmk provider. + """ + pass + + @staticmethod + def _format_alert( + event: dict, provider_instance: BaseProvider = None + ) -> AlertDto | list[AlertDto]: + """ + Service alerts and Host alerts have different fields, so we are mapping the fields based on the event type. + """ + + def _check_values(value): + if value not in event or event.get(value) == "": + return None + return event.get(value) + + """ Service alerts don't have a status field, so we are mapping the status based on the severity. """ - def _set_severity(status): - if status == "UP": - return AlertSeverity.INFO - elif status == "DOWN": - return AlertSeverity.CRITICAL - elif status == "UNREACH": - return AlertSeverity.CRITICAL - - alert = AlertDto( - id=_check_values("id"), - name=_check_values("check_command"), - description=_check_values("summary"), - severity=CheckmkProvider.SEVERITIES_MAP.get(event.get("severity"), _set_severity(event.get("status"))), - status=CheckmkProvider.STATUS_MAP.get(event.get("status"), AlertStatus.FIRING), - host=_check_values("host"), - alias=_check_values("alias"), - address=_check_values("address"), - service_name=_check_values("service"), - source=["checkmk"], - current_event=_check_values("event"), - output=_check_values("output"), - long_output=_check_values("long_output"), - path_url=_check_values("url"), - perf_data=_check_values("perf_data"), - site=_check_values("site"), - what=_check_values("what"), - notification_type=_check_values("notification_type"), - contact_name=_check_values("contact_name"), - contact_email=_check_values("contact_email"), - contact_pager=_check_values("contact_pager"), - date=_check_values("date"), - lastReceived=_check_values("short_date_time"), - long_date=_check_values("long_date_time"), - ) - - return alert - + + def _set_severity(status): + if status == "UP": + return AlertSeverity.INFO + elif status == "DOWN": + return AlertSeverity.CRITICAL + elif status == "UNREACH": + return AlertSeverity.CRITICAL + + alert = AlertDto( + id=_check_values("id"), + name=_check_values("check_command"), + description=_check_values("summary"), + severity=CheckmkProvider.SEVERITIES_MAP.get( + event.get("severity"), _set_severity(event.get("status")) + ), + status=CheckmkProvider.STATUS_MAP.get( + event.get("status"), AlertStatus.FIRING + ), + host=_check_values("host"), + alias=_check_values("alias"), + address=_check_values("address"), + service_name=_check_values("service"), + source=["checkmk"], + current_event=_check_values("event"), + output=_check_values("output"), + long_output=_check_values("long_output"), + path_url=_check_values("url"), + perf_data=_check_values("perf_data"), + site=_check_values("site"), + what=_check_values("what"), + notification_type=_check_values("notification_type"), + contact_name=_check_values("contact_name"), + contact_email=_check_values("contact_email"), + contact_pager=_check_values("contact_pager"), + date=_check_values("date"), + lastReceived=_check_values("short_date_time"), + long_date=_check_values("long_date_time"), + ) + + return alert + + if __name__ == "__main__": - pass + pass diff --git a/keep/providers/cilium_provider/cilium_provider.py b/keep/providers/cilium_provider/cilium_provider.py index 522bc9218..42d992cea 100644 --- a/keep/providers/cilium_provider/cilium_provider.py +++ b/keep/providers/cilium_provider/cilium_provider.py @@ -31,6 +31,7 @@ class CiliumProvider(BaseTopologyProvider): PROVIDER_TAGS = ["topology"] PROVIDER_DISPLAY_NAME = "Cilium" + PROVIDER_CATEGORY = ["Cloud Infrastructure", "Security"] def __init__( self, context_manager: ContextManager, provider_id: str, config: ProviderConfig diff --git a/keep/providers/clickhouse_provider/clickhouse_provider.py b/keep/providers/clickhouse_provider/clickhouse_provider.py index 4b35cefee..2b9d2c9f8 100644 --- a/keep/providers/clickhouse_provider/clickhouse_provider.py +++ b/keep/providers/clickhouse_provider/clickhouse_provider.py @@ -6,7 +6,6 @@ import os import pydantic - from clickhouse_driver import connect from clickhouse_driver.dbapi.extras import DictCursor @@ -21,7 +20,11 @@ class ClickhouseProviderAuthConfig: metadata={"required": True, "description": "Clickhouse username"} ) password: str = dataclasses.field( - metadata={"required": True, "description": "Clickhouse password", "sensitive": True} + metadata={ + "required": True, + "description": "Clickhouse password", + "sensitive": True, + } ) host: str = dataclasses.field( metadata={"required": True, "description": "Clickhouse hostname"} @@ -30,7 +33,8 @@ class ClickhouseProviderAuthConfig: metadata={"required": True, "description": "Clickhouse port"} ) database: str | None = dataclasses.field( - metadata={"required": False, "description": "Clickhouse database name"}, default=None + metadata={"required": False, "description": "Clickhouse database name"}, + default=None, ) @@ -38,6 +42,7 @@ class ClickhouseProvider(BaseProvider): """Enrich alerts with data from Clickhouse.""" PROVIDER_DISPLAY_NAME = "Clickhouse" + PROVIDER_CATEGORY = ["Database"] PROVIDER_SCOPES = [ ProviderScope( @@ -60,13 +65,13 @@ def validate_scopes(self): """ try: client = self.__generate_client() - + cursor = client.cursor() - cursor.execute('SHOW TABLES') - + cursor.execute("SHOW TABLES") + tables = cursor.fetchall() self.logger.info(f"Tables: {tables}") - + cursor.close() client.close() @@ -88,11 +93,11 @@ def __generate_client(self): clickhouse_driver.Connection: Clickhouse connection object """ - user=self.authentication_config.username - password=self.authentication_config.password - host=self.authentication_config.host - database=self.authentication_config.database - port=self.authentication_config.port + user = self.authentication_config.username + password = self.authentication_config.password + host = self.authentication_config.host + database = self.authentication_config.database + port = self.authentication_config.port dsn = f"clickhouse://{user}:{password}@{host}:{port}/{database}" @@ -121,9 +126,7 @@ def _query(self, query="", single_row=False, **kwargs: dict) -> list | tuple: """ return self._notify(query=query, single_row=single_row, **kwargs) - def _notify( - self, query="", single_row=False, **kwargs: dict - ) -> list | tuple: + def _notify(self, query="", single_row=False, **kwargs: dict) -> list | tuple: """ Executes a query against the Clickhouse database. @@ -160,5 +163,7 @@ def _notify( workflow_id="test", ) clickhouse_provider = ClickhouseProvider(context_manager, "clickhouse-prod", config) - results = clickhouse_provider.query(query="SELECT * FROM logs_table ORDER BY timestamp DESC LIMIT 1") + results = clickhouse_provider.query( + query="SELECT * FROM logs_table ORDER BY timestamp DESC LIMIT 1" + ) print(results) diff --git a/keep/providers/cloudwatch_provider/cloudwatch_provider.py b/keep/providers/cloudwatch_provider/cloudwatch_provider.py index be200f8ca..7552edeed 100644 --- a/keep/providers/cloudwatch_provider/cloudwatch_provider.py +++ b/keep/providers/cloudwatch_provider/cloudwatch_provider.py @@ -63,7 +63,8 @@ class CloudwatchProviderAuthConfig: class CloudwatchProvider(BaseProvider): """Push alarms from AWS Cloudwatch to Keep.""" - PROVIDER_DISPLAY_NAME = "Cloudwatch" + PROVIDER_DISPLAY_NAME = "CloudWatch" + PROVIDER_CATEGORY = ["Cloud Infrastructure", "Monitoring"] PROVIDER_SCOPES = [ ProviderScope( diff --git a/keep/providers/coralogix_provider/coralogix_provider.py b/keep/providers/coralogix_provider/coralogix_provider.py index 1ed543dde..59aa4faa0 100644 --- a/keep/providers/coralogix_provider/coralogix_provider.py +++ b/keep/providers/coralogix_provider/coralogix_provider.py @@ -44,7 +44,7 @@ class CoralogixProvider(BaseProvider): PROVIDER_DISPLAY_NAME = "Coralogix" PROVIDER_TAGS = ["alert"] - + PROVIDER_CATEGORY = ["Monitoring"] FINGERPRINT_FIELDS = ["alertUniqueIdentifier"] def __init__( diff --git a/keep/providers/datadog_provider/datadog_provider.py b/keep/providers/datadog_provider/datadog_provider.py index e19881f5d..5710433ae 100644 --- a/keep/providers/datadog_provider/datadog_provider.py +++ b/keep/providers/datadog_provider/datadog_provider.py @@ -102,6 +102,7 @@ class DatadogProviderAuthConfig: class DatadogProvider(BaseTopologyProvider): """Pull/push alerts from Datadog.""" + PROVIDER_CATEGORY = ["Monitoring"] PROVIDER_DISPLAY_NAME = "Datadog" OAUTH2_URL = os.environ.get("DATADOG_OAUTH2_URL") DATADOG_CLIENT_ID = os.environ.get("DATADOG_CLIENT_ID") diff --git a/keep/providers/discord_provider/discord_provider.py b/keep/providers/discord_provider/discord_provider.py index a9bd7dfb3..c35cf503a 100644 --- a/keep/providers/discord_provider/discord_provider.py +++ b/keep/providers/discord_provider/discord_provider.py @@ -30,6 +30,7 @@ class DiscordProvider(BaseProvider): """Send alert message to Discord.""" PROVIDER_DISPLAY_NAME = "Discord" + PROVIDER_CATEGORY = ["Collaboration"] def __init__( self, context_manager: ContextManager, provider_id: str, config: ProviderConfig diff --git a/keep/providers/dynatrace_provider/dynatrace_provider.py b/keep/providers/dynatrace_provider/dynatrace_provider.py index 67e702a36..5f61c22e6 100644 --- a/keep/providers/dynatrace_provider/dynatrace_provider.py +++ b/keep/providers/dynatrace_provider/dynatrace_provider.py @@ -55,6 +55,8 @@ class DynatraceProvider(BaseProvider): Dynatrace provider class. """ + PROVIDER_CATEGORY = ["Monitoring"] + PROVIDER_SCOPES = [ ProviderScope( name="problems.read", diff --git a/keep/providers/elastic_provider/elastic_provider.py b/keep/providers/elastic_provider/elastic_provider.py index 1b3248fc6..0de3db3a2 100644 --- a/keep/providers/elastic_provider/elastic_provider.py +++ b/keep/providers/elastic_provider/elastic_provider.py @@ -47,6 +47,7 @@ class ElasticProvider(BaseProvider): """Enrich alerts with data from Elasticsearch.""" PROVIDER_DISPLAY_NAME = "Elastic" + PROVIDER_CATEGORY = ["Monitoring", "Database"] def __init__( self, context_manager: ContextManager, provider_id: str, config: ProviderConfig diff --git a/keep/providers/gcpmonitoring_provider/gcpmonitoring_provider.py b/keep/providers/gcpmonitoring_provider/gcpmonitoring_provider.py index a78280944..8230ccff6 100644 --- a/keep/providers/gcpmonitoring_provider/gcpmonitoring_provider.py +++ b/keep/providers/gcpmonitoring_provider/gcpmonitoring_provider.py @@ -75,7 +75,7 @@ class GcpmonitoringProvider(BaseProvider): "ERROR": AlertSeverity.HIGH, "WARNING": AlertSeverity.WARNING, } - + PROVIDER_CATEGORY = ["Monitoring", "Cloud Infrastructure"] STATUS_MAP = { "CLOSED": AlertStatus.RESOLVED, "OPEN": AlertStatus.FIRING, diff --git a/keep/providers/github_provider/github_provider.py b/keep/providers/github_provider/github_provider.py index c4d524a8d..8a9172126 100644 --- a/keep/providers/github_provider/github_provider.py +++ b/keep/providers/github_provider/github_provider.py @@ -33,6 +33,7 @@ class GithubProvider(BaseProvider): """ PROVIDER_DISPLAY_NAME = "GitHub" + PROVIDER_CATEGORY = ["Developer Tools"] def __init__( self, context_manager: ContextManager, provider_id: str, config: ProviderConfig diff --git a/keep/providers/gitlab_provider/gitlab_provider.py b/keep/providers/gitlab_provider/gitlab_provider.py index 90563a15e..9d04122f7 100644 --- a/keep/providers/gitlab_provider/gitlab_provider.py +++ b/keep/providers/gitlab_provider/gitlab_provider.py @@ -50,9 +50,10 @@ class GitlabProvider(BaseProvider): ] PROVIDER_TAGS = ["ticketing"] PROVIDER_DISPLAY_NAME = "GitLab" + PROVIDER_CATEGORY = ["Developer Tools"] def __init__( - self, context_manager: ContextManager, provider_id: str, config: ProviderConfig + self, context_manager: ContextManager, provider_id: str, config: ProviderConfig ): self._host = None super().__init__(context_manager, provider_id, config) @@ -76,12 +77,10 @@ def validate_scopes(self): try: resp.raise_for_status() scopes = { - "api": ("Missing api scope", True)['api' in resp.json()['scopes']] + "api": ("Missing api scope", True)["api" in resp.json()["scopes"]] } except HTTPError as e: - scopes = { - "api": str(e) - } + scopes = {"api": str(e)} return scopes def validate_config(self): @@ -97,7 +96,7 @@ def gitlab_host(self): # if the user explicitly supplied a host with http/https, use it if self.authentication_config.host.startswith( - "http://" + "http://" ) or self.authentication_config.host.startswith("https://"): self._host = self.authentication_config.host return self.authentication_config.host.rstrip("/") @@ -143,15 +142,32 @@ def __build_params_from_kwargs(self, kwargs: dict): params[param] = kwargs[param] return params - def _notify(self, id: str, title: str, description: str = "", labels: str = "", issue_type: str = "issue", - **kwargs: dict): - id = urllib.parse.quote(id, safe='') + def _notify( + self, + id: str, + title: str, + description: str = "", + labels: str = "", + issue_type: str = "issue", + **kwargs: dict, + ): + id = urllib.parse.quote(id, safe="") print(id) params = self.__build_params_from_kwargs( - kwargs={**kwargs, 'title': title, 'description': description, 'labels': labels, 'issue_type': issue_type}) + kwargs={ + **kwargs, + "title": title, + "description": description, + "labels": labels, + "issue_type": issue_type, + } + ) print(self.gitlab_host) - resp = requests.post(f"{self.gitlab_host}/api/v4/projects/{id}/issues", headers=self.__get_auth_header(), - params=params) + resp = requests.post( + f"{self.gitlab_host}/api/v4/projects/{id}/issues", + headers=self.__get_auth_header(), + params=params, + ) try: resp.raise_for_status() except HTTPError as e: diff --git a/keep/providers/gitlabpipelines_provider/gitlabpipelines_provider.py b/keep/providers/gitlabpipelines_provider/gitlabpipelines_provider.py index 2aa34585c..64431c110 100644 --- a/keep/providers/gitlabpipelines_provider/gitlabpipelines_provider.py +++ b/keep/providers/gitlabpipelines_provider/gitlabpipelines_provider.py @@ -32,6 +32,7 @@ class GitlabpipelinesProvider(BaseProvider): """Enrich alerts with data from GitLab Pipelines.""" PROVIDER_DISPLAY_NAME = "GitLab Pipelines" + PROVIDER_CATEGORY = ["Developer Tools"] def __init__( self, context_manager: ContextManager, provider_id: str, config: ProviderConfig @@ -49,12 +50,7 @@ def dispose(self): """ pass - def _notify( - self, - gitlab_url: str = "", - gitlab_method: str = "", - **kwargs - ): + def _notify(self, gitlab_url: str = "", gitlab_method: str = "", **kwargs): url = gitlab_url method = gitlab_method.upper() diff --git a/keep/providers/gke_provider/gke_provider.py b/keep/providers/gke_provider/gke_provider.py index 2b44742e2..92d5016b7 100644 --- a/keep/providers/gke_provider/gke_provider.py +++ b/keep/providers/gke_provider/gke_provider.py @@ -44,7 +44,8 @@ class GkeProviderAuthConfig: class GkeProvider(BaseProvider): """Enrich alerts with data from GKE.""" - PROVIDER_DISPLAY_NAME = "GKE" + PROVIDER_DISPLAY_NAME = "Google Kubernetes Engine" + PROVIDER_CATEGORY = ["Cloud Infrastructure"] PROVIDER_SCOPES = [ ProviderScope( @@ -87,9 +88,9 @@ def validate_scopes(self): scopes["roles/container.viewer"] = True except Exception as e: if "404" in str(e): - scopes[ - "roles/container.viewer" - ] = "Cluster not found (404 from GKE), please check the cluster name and region" + scopes["roles/container.viewer"] = ( + "Cluster not found (404 from GKE), please check the cluster name and region" + ) elif "403" in str(e): scopes["roles/container.viewer"] = "Permission denied (403 from GKE)" else: diff --git a/keep/providers/google_chat_provider/google_chat_provider.py b/keep/providers/google_chat_provider/google_chat_provider.py index 7eef362e6..1b749dc08 100644 --- a/keep/providers/google_chat_provider/google_chat_provider.py +++ b/keep/providers/google_chat_provider/google_chat_provider.py @@ -1,9 +1,9 @@ +import dataclasses import http import os import time import pydantic -import dataclasses import requests from keep.contextmanager.contextmanager import ContextManager @@ -32,9 +32,10 @@ class GoogleChatProvider(BaseProvider): PROVIDER_DISPLAY_NAME = "Google Chat" PROVIDER_TAGS = ["messaging"] + PROVIDER_CATEGORY = ["Collaboration"] def __init__( - self, context_manager: ContextManager, provider_id: str, config: ProviderConfig + self, context_manager: ContextManager, provider_id: str, config: ProviderConfig ): super().__init__(context_manager, provider_id, config) @@ -75,7 +76,9 @@ def __send_message(url, body, headers, retries=3): if resp.status_code == http.HTTPStatus.OK: return resp - self.logger.warning(f"Attempt {attempt + 1} failed with status code {resp.status_code}") + self.logger.warning( + f"Attempt {attempt + 1} failed with status code {resp.status_code}" + ) except requests.exceptions.RequestException as e: self.logger.error(f"Attempt {attempt + 1} failed: {e}") @@ -83,7 +86,9 @@ def __send_message(url, body, headers, retries=3): if attempt < retries - 1: time.sleep(1) - raise requests.exceptions.RequestException(f"Failed to notify message after {retries} attempts") + raise requests.exceptions.RequestException( + f"Failed to notify message after {retries} attempts" + ) payload = { "text": message, @@ -93,7 +98,9 @@ def __send_message(url, body, headers, retries=3): response = __send_message(webhook_url, body=payload, headers=request_headers) if response.status_code != http.HTTPStatus.OK: - raise ProviderException(f"Failed to notify message to Google Chat: {response.text}") + raise ProviderException( + f"Failed to notify message to Google Chat: {response.text}" + ) self.logger.debug("Alert message sent to Google Chat successfully") return "Alert message sent to Google Chat successfully" diff --git a/keep/providers/grafana_incident_provider/grafana_incident_provider.py b/keep/providers/grafana_incident_provider/grafana_incident_provider.py index c8474e9b0..8f2522d6b 100644 --- a/keep/providers/grafana_incident_provider/grafana_incident_provider.py +++ b/keep/providers/grafana_incident_provider/grafana_incident_provider.py @@ -3,22 +3,23 @@ """ import dataclasses -import pydantic +from urllib.parse import urljoin +import pydantic import requests -from urllib.parse import urljoin - from keep.api.models.alert import AlertDto, AlertSeverity, AlertStatus from keep.contextmanager.contextmanager import ContextManager from keep.providers.base.base_provider import BaseProvider from keep.providers.models.provider_config import ProviderConfig, ProviderScope + @pydantic.dataclasses.dataclass class GrafanaIncidentProviderAuthConfig: """ GrafanaIncidentProviderAuthConfig is a class that allows to authenticate in Grafana Incident. """ + host_url: str = dataclasses.field( metadata={ "required": True, @@ -37,6 +38,7 @@ class GrafanaIncidentProviderAuthConfig: default=None, ) + class GrafanaIncidentProvider(BaseProvider): PROVIDER_DISPLAY_NAME = "Grafana Incident" PROVIDER_TAGS = ["alert"] @@ -47,6 +49,7 @@ class GrafanaIncidentProvider(BaseProvider): description="User is Authenticated", ), ] + PROVIDER_CATEGORY = ["Incident Management"] SEVERITIES_MAP = { "Pending": AlertSeverity.INFO, @@ -55,19 +58,16 @@ class GrafanaIncidentProvider(BaseProvider): "Minor": AlertSeverity.LOW, } - STATUS_MAP = { - "active": AlertStatus.FIRING, - "resolved": AlertStatus.RESOLVED - } + STATUS_MAP = {"active": AlertStatus.FIRING, "resolved": AlertStatus.RESOLVED} def __init__( - self, context_manager: ContextManager, provider_id: str, config: ProviderConfig + self, context_manager: ContextManager, provider_id: str, config: ProviderConfig ): super().__init__(context_manager, provider_id, config) def dispose(self): pass - + def validate_config(self): """ Validate the configuration of the provider. @@ -91,49 +91,61 @@ def validate_scopes(self) -> dict[str, bool | str]: """ try: response = requests.post( - urljoin(self.authentication_config.host_url, "/api/plugins/grafana-incident-app/resources/api/v1/IncidentsService.QueryIncidentPreviews"), + urljoin( + self.authentication_config.host_url, + "/api/plugins/grafana-incident-app/resources/api/v1/IncidentsService.QueryIncidentPreviews", + ), headers=self.__get_headers(), json={ "query": { "limit": 10, "orderDirection": "DESC", - "orderField": "createdTime" + "orderField": "createdTime", } - } + }, ) if response.status_code == 200: return {"authenticated": True} else: self.logger.error(f"Failed to validate scopes: {response.status_code}") - scopes = {"authenticated": f"Unable to query incidents: {response.status_code}"} + scopes = { + "authenticated": f"Unable to query incidents: {response.status_code}" + } except Exception as e: self.logger.error(f"Failed to validate scopes: {e}") scopes = {"authenticated": f"Unable to query incidents: {e}"} return scopes - + def _get_alerts(self) -> list[AlertDto]: """ Get the alerts from Grafana Incident. """ try: response = requests.post( - urljoin(self.authentication_config.host_url, "/api/plugins/grafana-incident-app/resources/api/v1/IncidentsService.QueryIncidentPreviews"), + urljoin( + self.authentication_config.host_url, + "/api/plugins/grafana-incident-app/resources/api/v1/IncidentsService.QueryIncidentPreviews", + ), headers=self.__get_headers(), json={ "query": { "limit": 10, "orderDirection": "DESC", - "orderField": "createdTime" + "orderField": "createdTime", } - } + }, ) if not response.ok: - self.logger.error(f"Failed to get incidents from grafana incident: {response.status_code}") - raise Exception(f"Failed to get incidents from grafana incident: {response.status_code} - {response.text}") - + self.logger.error( + f"Failed to get incidents from grafana incident: {response.status_code}" + ) + raise Exception( + f"Failed to get incidents from grafana incident: {response.status_code} - {response.text}" + ) + return [ AlertDto( id=incident["incidentID"], @@ -158,7 +170,7 @@ def _get_alerts(self) -> list[AlertDto]: incidentMembershipPreview=incident["incidentMembershipPreview"], fieldValues=incident["fieldValues"], version=incident["version"], - source=["grafana_incident"] + source=["grafana_incident"], ) for incident in response.json()["incidentPreviews"] ] @@ -166,7 +178,8 @@ def _get_alerts(self) -> list[AlertDto]: except Exception as e: self.logger.error(f"Failed to get incidents from grafana incident: {e}") raise Exception(f"Failed to get incidents from grafana incident: {e}") - + + if __name__ == "__main__": import logging @@ -182,8 +195,10 @@ def _get_alerts(self) -> list[AlertDto]: api_token = os.getenv("GRAFANA_SERVICE_ACCOUNT_TOKEN") if host_url is None or api_token is None: - raise Exception("GRAFANA_HOST_URL and GRAFANA_SERVICE_ACCOUNT_TOKEN environment variables are required") - + raise Exception( + "GRAFANA_HOST_URL and GRAFANA_SERVICE_ACCOUNT_TOKEN environment variables are required" + ) + config = ProviderConfig( description="Grafana Incident Provider", authentication={ diff --git a/keep/providers/grafana_oncall_provider/grafana_oncall_provider.py b/keep/providers/grafana_oncall_provider/grafana_oncall_provider.py index d264f77cb..91e0a2fd2 100644 --- a/keep/providers/grafana_oncall_provider/grafana_oncall_provider.py +++ b/keep/providers/grafana_oncall_provider/grafana_oncall_provider.py @@ -1,6 +1,7 @@ """ Grafana Provider is a class that allows to ingest/digest data from Grafana. """ + import dataclasses import random from typing import Literal @@ -42,6 +43,7 @@ class GrafanaOncallProvider(BaseProvider): """ PROVIDER_DISPLAY_NAME = "Grafana OnCall" + PROVIDER_CATEGORY = ["Incident Management"] API_URI = "api/plugins/grafana-incident-app/resources/api" provider_description = "Grafana OnCall is a SaaS incident management solution that helps you resolve incidents faster." diff --git a/keep/providers/grafana_provider/grafana_provider.py b/keep/providers/grafana_provider/grafana_provider.py index 31c9f442b..703fac575 100644 --- a/keep/providers/grafana_provider/grafana_provider.py +++ b/keep/providers/grafana_provider/grafana_provider.py @@ -48,6 +48,7 @@ class GrafanaProvider(BaseProvider): PROVIDER_DISPLAY_NAME = "Grafana" """Pull/Push alerts from Grafana.""" + PROVIDER_CATEGORY = ["Monitoring", "Developer Tools"] KEEP_GRAFANA_WEBHOOK_INTEGRATION_NAME = "keep-grafana-webhook-integration" FINGERPRINT_FIELDS = ["fingerprint"] diff --git a/keep/providers/graylog_provider/graylog_provider.py b/keep/providers/graylog_provider/graylog_provider.py index a59156d65..d35037c53 100644 --- a/keep/providers/graylog_provider/graylog_provider.py +++ b/keep/providers/graylog_provider/graylog_provider.py @@ -1,12 +1,13 @@ """ Graylog Provider is a class that allows to install webhooks in Graylog. """ + # Documentation for older versions of graylog: https://github.com/Graylog2/documentation import dataclasses import math import uuid -from datetime import datetime, timezone, timedelta +from datetime import datetime, timedelta, timezone from typing import List from urllib.parse import urlencode, urljoin, urlparse @@ -57,6 +58,7 @@ class GraylogProviderAuthConfig: class GraylogProvider(BaseProvider): """Install Webhooks and receive alerts from Graylog.""" + PROVIDER_CATEGORY = ["Monitoring"] webhook_description = "" webhook_template = "" webhook_markdown = """ @@ -231,18 +233,16 @@ def validate_scopes(self) -> dict[str, bool | str]: def __get_graylog_version(self) -> str: self.logger.info("Getting graylog version info") try: - version_response = requests.get( - url=self.__get_url(), - headers=self._headers - ) + version_response = requests.get(url=self.__get_url(), headers=self._headers) if version_response.status_code != 200: raise Exception(version_response.text) version = version_response.json()["version"].strip() self.logger.info(f"We are working with Graylog version: {version}") return version except Exception as e: - self.logger.error("Error while getting Graylog Version", extra={"exception": str(e)}) - + self.logger.error( + "Error while getting Graylog Version", extra={"exception": str(e)} + ) def __get_url_whitelist(self): try: @@ -485,16 +485,11 @@ def setup_webhook( # We need to clean up the previously installed notification existing_notification_id = notification["notifications"][0]["id"] - self.__delete_notification( - notification_id=existing_notification_id - ) + self.__delete_notification(notification_id=existing_notification_id) self.logger.info("Creating new notification") if self.is_v4: - config = { - "type": "http-notification-v1", - "url": keep_api_url - } + config = {"type": "http-notification-v1", "url": keep_api_url} else: config = { "type": "http-notification-v2", @@ -520,7 +515,10 @@ def setup_webhook( ) for event_definition in event_definitions: - if not self.is_v4 and event_definition["_scope"] == "SYSTEM_NOTIFICATION_EVENT": + if ( + not self.is_v4 + and event_definition["_scope"] == "SYSTEM_NOTIFICATION_EVENT" + ): self.logger.info("Skipping SYSTEM_NOTIFICATION_EVENT") continue self.logger.info(f"Updating event with ID: {event_definition['id']}") @@ -571,15 +569,18 @@ def __map_event_to_alert(event: dict) -> AlertDto: return alert @staticmethod - def _format_alert(event: dict, provider_instance: BaseProvider | None = None) -> AlertDto: + def _format_alert( + event: dict, provider_instance: BaseProvider | None = None + ) -> AlertDto: return GraylogProvider.__map_event_to_alert(event=event) @classmethod def simulate_alert(cls) -> dict: - from keep.providers.graylog_provider.alerts_mock import ALERTS import random import string + from keep.providers.graylog_provider.alerts_mock import ALERTS + # Use the provided ALERTS structure alert_data = ALERTS.copy() diff --git a/keep/providers/ilert_provider/ilert_provider.py b/keep/providers/ilert_provider/ilert_provider.py index 748e67c63..8cb839fa9 100644 --- a/keep/providers/ilert_provider/ilert_provider.py +++ b/keep/providers/ilert_provider/ilert_provider.py @@ -61,6 +61,7 @@ class IlertProvider(BaseProvider): ProviderScope("read_permission", "Read permission", mandatory=True), ProviderScope("write_permission", "Write permission", mandatory=False), ] + PROVIDER_CATEGORY = ["Incident Management"] SEVERITIES_MAP = { "MAJOR_OUTAGE": AlertSeverity.CRITICAL, diff --git a/keep/providers/incidentio_provider/incidentio_provider.py b/keep/providers/incidentio_provider/incidentio_provider.py index 77cfe634e..acd7ecb12 100644 --- a/keep/providers/incidentio_provider/incidentio_provider.py +++ b/keep/providers/incidentio_provider/incidentio_provider.py @@ -25,6 +25,7 @@ class IncidentioProviderAuthConfig: """ Incidentio authentication configuration. """ + incidentIoApiKey: str = dataclasses.field( metadata={ "required": True, @@ -39,6 +40,7 @@ class IncidentioProvider(BaseProvider): """Receive Incidents from Incidentio.""" PROVIDER_DISPLAY_NAME = "incident.io" + PROVIDER_CATEGORY = ["Incident Management"] PROVIDER_SCOPES = [ ProviderScope( @@ -52,7 +54,7 @@ class IncidentioProvider(BaseProvider): description="User has read access", mandatory=True, alias="can_read", - ) + ), ] SEVERITIES_MAP = { @@ -60,7 +62,7 @@ class IncidentioProvider(BaseProvider): "Major": AlertSeverity.HIGH, "Info": AlertSeverity.INFO, "Critical": AlertSeverity.CRITICAL, - "Minor": AlertSeverity.LOW + "Minor": AlertSeverity.LOW, } STATUS_MAP = { @@ -71,11 +73,11 @@ class IncidentioProvider(BaseProvider): "live": AlertStatus.FIRING, "learning": AlertStatus.PENDING, "closed": AlertStatus.RESOLVED, - "paused": AlertStatus.SUPPRESSED + "paused": AlertStatus.SUPPRESSED, } def __init__( - self, context_manager: ContextManager, provider_id: str, config: ProviderConfig + self, context_manager: ContextManager, provider_id: str, config: ProviderConfig ): super().__init__(context_manager, provider_id, config) @@ -121,7 +123,7 @@ def __get_headers(self): Building the headers for api requests """ return { - 'Authorization': f'Bearer {self.authentication_config.incidentIoApiKey}', + "Authorization": f"Bearer {self.authentication_config.incidentIoApiKey}", } def validate_scopes(self) -> dict[str, bool | str]: @@ -138,36 +140,58 @@ def validate_scopes(self) -> dict[str, bool | str]: return {"authenticated": True, "read_access": True} else: self.logger.error(f"Failed to validate scopes: {response.status_code}") - scopes = {"authenticated": "Unable to query incidents: {response.status_code}", "read_access": False} + scopes = { + "authenticated": "Unable to query incidents: {response.status_code}", + "read_access": False, + } except Exception as e: - self.logger.error("Error getting IncidentIO scopes:", extra={"exception": str(e)}) - scopes = {"authenticated": "Unable to query incidents: {e}", "read_access": False} + self.logger.error( + "Error getting IncidentIO scopes:", extra={"exception": str(e)} + ) + scopes = { + "authenticated": "Unable to query incidents: {e}", + "read_access": False, + } return scopes def _query(self, incident_id, **kwargs) -> AlertDto: """query IncidentIO Incident""" - self.logger.info("Querying IncidentIO incident", - extra={ - "incident_id": incident_id, - **kwargs, - }, ) + self.logger.info( + "Querying IncidentIO incident", + extra={ + "incident_id": incident_id, + **kwargs, + }, + ) try: response = requests.get( url=self.__get_url(paths=["incidents", incident_id]), headers=self.__get_headers(), ) except Exception as e: - self.logger.error("Error while fetching Incident", - extra={"incident_id": incident_id, "kwargs": kwargs, "exception": str(e)}) + self.logger.error( + "Error while fetching Incident", + extra={ + "incident_id": incident_id, + "kwargs": kwargs, + "exception": str(e), + }, + ) raise e else: if response.ok: res = response.json() return self.__map_alert_to_AlertDTO({"event": res}) else: - self.logger.error("Error while fetching Incident", - extra={"incident_id": incident_id, "kwargs": kwargs, "res": response.text}) + self.logger.error( + "Error while fetching Incident", + extra={ + "incident_id": incident_id, + "kwargs": kwargs, + "res": response.text, + }, + ) def _get_alerts(self) -> list[AlertDto]: alerts = [] @@ -175,28 +199,35 @@ def _get_alerts(self) -> list[AlertDto]: while True: try: - params = {'page_size': 100} + params = {"page_size": 100} if next_page: - params['after'] = next_page - - response = requests.get(self.__get_url(paths=["incidents"]), headers=self.__get_headers(), params=params, - timeout=15) + params["after"] = next_page + + response = requests.get( + self.__get_url(paths=["incidents"]), + headers=self.__get_headers(), + params=params, + timeout=15, + ) response.raise_for_status() except requests.RequestException as e: - self.logger.error("Error getting IncidentIO scopes:", extra={"exception": str(e)}) + self.logger.error( + "Error getting IncidentIO scopes:", extra={"exception": str(e)} + ) raise e else: data = response.json() try: - for incident in data.get('incidents', []): - alerts.append( - self.__map_alert_to_AlertDTO(incident) - ) + for incident in data.get("incidents", []): + alerts.append(self.__map_alert_to_AlertDTO(incident)) except Exception as e: - self.logger.error("Error while mapping incidents to AlertDTO", extra={"exception": str(e)}) + self.logger.error( + "Error while mapping incidents to AlertDTO", + extra={"exception": str(e)}, + ) raise e - pagination_meta = data.get('pagination_meta', {}) - next_page = pagination_meta.get('after') + pagination_meta = data.get("pagination_meta", {}) + next_page = pagination_meta.get("after") if not next_page: break @@ -205,21 +236,26 @@ def _get_alerts(self) -> list[AlertDto]: def __map_alert_to_AlertDTO(self, incident) -> AlertDto: return AlertDto( - id=incident['id'], - fingerprint=incident['id'], - name=incident['name'], - status=IncidentioProvider.STATUS_MAP[incident['incident_status']["category"]], + id=incident["id"], + fingerprint=incident["id"], + name=incident["name"], + status=IncidentioProvider.STATUS_MAP[ + incident["incident_status"]["category"] + ], severity=IncidentioProvider.SEVERITIES_MAP.get( - incident.get("severity", {}).get("name", "minor"), AlertSeverity.WARNING), - lastReceived=incident.get('created_at'), - description=incident.get('summary', ""), + incident.get("severity", {}).get("name", "minor"), AlertSeverity.WARNING + ), + lastReceived=incident.get("created_at"), + description=incident.get("summary", ""), apiKeyRef=incident["creator"]["api_key"]["id"], assignee=", ".join( - assignment["role"]["name"] for assignment in - incident["incident_role_assignments"]), - url=incident.get("permalink", "https://app.incident.io/") + assignment["role"]["name"] + for assignment in incident["incident_role_assignments"] + ), + url=incident.get("permalink", "https://app.incident.io/"), ) + if __name__ == "__main__": import logging @@ -235,9 +271,7 @@ def __map_alert_to_AlertDTO(self, incident) -> AlertDto: config = ProviderConfig( description="Incidentio Provider", - authentication={ - "incidentIoApiKey": api_key - }, + authentication={"incidentIoApiKey": api_key}, ) provider = IncidentioProvider( diff --git a/keep/providers/incidentmanager_provider/incidentmanager_provider.py b/keep/providers/incidentmanager_provider/incidentmanager_provider.py index 6abdc00db..06fa5258d 100644 --- a/keep/providers/incidentmanager_provider/incidentmanager_provider.py +++ b/keep/providers/incidentmanager_provider/incidentmanager_provider.py @@ -61,6 +61,8 @@ class IncidentmanagerProviderAuthConfig: class IncidentmanagerProvider(BaseProvider): """Push incidents from AWS IncidentManager to Keep.""" + PROVIDER_CATEGORY = ["Incident Management"] + PROVIDER_SCOPES = [ ProviderScope( name="ssm-incidents:ListIncidentRecords", diff --git a/keep/providers/jira_provider/jira_provider.py b/keep/providers/jira_provider/jira_provider.py index b79216594..e0cf9048d 100644 --- a/keep/providers/jira_provider/jira_provider.py +++ b/keep/providers/jira_provider/jira_provider.py @@ -1,6 +1,7 @@ """ JiracloudProvider is a class that implements the BaseProvider interface for Jira updates. """ + import dataclasses import json from typing import List @@ -51,6 +52,8 @@ class JiraProviderAuthConfig: class JiraProvider(BaseProvider): """Enrich alerts with Jira tickets.""" + PROVIDER_CATEGORY = ["Ticketing"] + PROVIDER_SCOPES = [ ProviderScope( name="BROWSE_PROJECTS", @@ -304,7 +307,7 @@ def __create_issue( return {"issue": response.json()} except Exception as e: raise ProviderException(f"Failed to create an issue: {e}") - + def __update_issue( self, issue_id: str, @@ -323,7 +326,7 @@ def __update_issue( url = self.__get_url(paths=["issue", issue_id]) - update = { } + update = {} if summary: update["summary"] = [{"set": summary}] @@ -340,7 +343,7 @@ def __update_issue( if custom_fields: update.update(custom_fields) - request_body = { "update": update } + request_body = {"update": update} response = requests.put( url=url, json=request_body, auth=self.__get_auth(), verify=False @@ -350,9 +353,7 @@ def __update_issue( if response.status_code != 204: response.raise_for_status() except Exception: - self.logger.exception( - "Failed to update an issue", extra=response.text - ) + self.logger.exception("Failed to update an issue", extra=response.text) raise ProviderException("Failed to update an issue") self.logger.info("Updated an issue!") return { @@ -362,7 +363,7 @@ def __update_issue( "self": self.__get_url(paths=["issue", issue_id]), } } - + except Exception as e: raise ProviderException(f"Failed to update an issue: {e}") @@ -389,7 +390,7 @@ def _extract_project_key_from_board_name(self, board_name: str): ) else: raise Exception("Could not fetch boards: " + boards_response.text) - + def _extract_issue_key_from_issue_id(self, issue_id: str): issue_key = requests.get( f"{self.jira_host}/rest/api/2/issue/{issue_id}", @@ -442,7 +443,7 @@ def _notify( self.logger.info("Updated a jira issue: " + str(result)) return result - + if not project_key: project_key = self._extract_project_key_from_board_name(board_name) if not project_key or not summary or not issue_type or not description: diff --git a/keep/providers/jiraonprem_provider/jiraonprem_provider.py b/keep/providers/jiraonprem_provider/jiraonprem_provider.py index f6f48379e..907b7069b 100644 --- a/keep/providers/jiraonprem_provider/jiraonprem_provider.py +++ b/keep/providers/jiraonprem_provider/jiraonprem_provider.py @@ -42,6 +42,8 @@ class JiraonpremProviderAuthConfig: class JiraonpremProvider(BaseProvider): """Enrich alerts with Jira tickets.""" + PROVIDER_CATEGORY = ["Ticketing"] + PROVIDER_SCOPES = [ ProviderScope( name="BROWSE_PROJECTS", @@ -323,7 +325,7 @@ def __create_issue( return {"issue": response.json()} except Exception as e: raise ProviderException(f"Failed to create an issue: {e}") - + def __update_issue( self, issue_id: str, @@ -343,7 +345,7 @@ def __update_issue( url = self.__get_url(paths=["issue", issue_id]) - update = { } + update = {} if summary: update["summary"] = [{"set": summary}] @@ -355,7 +357,9 @@ def __update_issue( update["priority"] = [{"set": {"name": priority}}] if components: - update["components"] = [{"set": [{"name": component} for component in components]}] + update["components"] = [ + {"set": [{"name": component} for component in components]} + ] if labels: update["labels"] = [{"set": label} for label in labels] @@ -377,11 +381,9 @@ def __update_issue( if response.status_code != 204: response.raise_for_status() except Exception: - self.logger.exception( - "Failed to update an issue", extra=response.text - ) + self.logger.exception("Failed to update an issue", extra=response.text) raise ProviderException("Failed to update an issue") - + result = { "issue": { "id": issue_id, @@ -392,7 +394,7 @@ def __update_issue( self.logger.info("Updated an issue!") return result - + except Exception as e: raise ProviderException(f"Failed to update an issue: {e}") @@ -457,7 +459,7 @@ def _extract_project_key_from_board_name(self, board_name: str): ) else: raise Exception("Could not fetch boards: " + boards_response.text) - + def _extract_issue_key_from_issue_id(self, issue_id: str): headers = { "Accept": "application/json", @@ -474,7 +476,7 @@ def _extract_issue_key_from_issue_id(self, issue_id: str): if issue_key.status_code == 200: return issue_key.json()["key"] else: - raise Exception("Could not fetch issue key: " + issue_key.text) + raise Exception("Could not fetch issue key: " + issue_key.text) def _notify( self, @@ -517,14 +519,14 @@ def _notify( result["ticket_url"] = f"{self.jira_host}/browse/{issue_key}" self.logger.info("Updated a jira issue: " + str(result)) - return result + return result if not project_key: project_key = self._extract_project_key_from_board_name(board_name) if not project_key or not summary or not issue_type or not description: raise ProviderException( f"Project key and summary are required! - {project_key}, {summary}, {issue_type}, {description}" - ) + ) result = self.__create_issue( project_key=project_key, diff --git a/keep/providers/kafka_provider/kafka_provider.py b/keep/providers/kafka_provider/kafka_provider.py index d79a1c293..993b9e4ab 100644 --- a/keep/providers/kafka_provider/kafka_provider.py +++ b/keep/providers/kafka_provider/kafka_provider.py @@ -1,6 +1,7 @@ """ Kafka Provider is a class that allows to ingest/digest data from Grafana. """ + import dataclasses import inspect import logging @@ -96,6 +97,8 @@ class KafkaProvider(BaseProvider): Kafka provider class. """ + PROVIDER_CATEGORY = ["Developer Tools", "Queues"] + PROVIDER_DISPLAY_NAME = "Kafka" PROVIDER_SCOPES = [ ProviderScope( @@ -186,9 +189,11 @@ def _get_conf(self): if self.authentication_config.username and self.authentication_config.password: basic_conf.update( { - "security_protocol": "SASL_SSL" - if self.authentication_config.username - else "PLAINTEXT", + "security_protocol": ( + "SASL_SSL" + if self.authentication_config.username + else "PLAINTEXT" + ), "sasl_mechanism": "PLAIN", "sasl_plain_username": self.authentication_config.username, "sasl_plain_password": self.authentication_config.password, diff --git a/keep/providers/kibana_provider/kibana_provider.py b/keep/providers/kibana_provider/kibana_provider.py index 98a7273e8..6bd7238cb 100644 --- a/keep/providers/kibana_provider/kibana_provider.py +++ b/keep/providers/kibana_provider/kibana_provider.py @@ -46,6 +46,7 @@ class KibanaProviderAuthConfig: class KibanaProvider(BaseProvider): """Enrich alerts with data from Kibana.""" + PROVIDER_CATEGORY = ["Monitoring", "Developer Tools"] DEFAULT_TIMEOUT = 10 WEBHOOK_PAYLOAD = json.dumps( { diff --git a/keep/providers/kubernetes_provider/kubernetes_provider.py b/keep/providers/kubernetes_provider/kubernetes_provider.py index 2dd85709b..fb7e5417e 100644 --- a/keep/providers/kubernetes_provider/kubernetes_provider.py +++ b/keep/providers/kubernetes_provider/kubernetes_provider.py @@ -1,13 +1,13 @@ -import pydantic import dataclasses +import datetime +import pydantic from kubernetes import client from kubernetes.client.rest import ApiException -import datetime from keep.contextmanager.contextmanager import ContextManager from keep.providers.base.base_provider import BaseProvider -from keep.providers.models.provider_config import ProviderScope, ProviderConfig +from keep.providers.models.provider_config import ProviderConfig, ProviderScope @pydantic.dataclasses.dataclass @@ -48,7 +48,7 @@ class KubernetesProvider(BaseProvider): provider_id: str PROVIDER_DISPLAY_NAME = "Kubernetes" - + PROVIDER_CATEGORY = ["Cloud Infrastructure", "Developer Tools"] PROVIDER_SCOPES = [ ProviderScope( name="connect_to_kubernetes", @@ -58,9 +58,7 @@ class KubernetesProvider(BaseProvider): ) ] - def __init__( - self, context_manager, provider_id: str, config: ProviderConfig - ): + def __init__(self, context_manager, provider_id: str, config: ProviderConfig): super().__init__(context_manager, provider_id, config) self.authentication_config = None self.validate_config() @@ -75,7 +73,9 @@ def validate_config(self): """ if self.config.authentication is None: self.config.authentication = {} - self.authentication_config = KubernetesProviderAuthConfig(**self.config.authentication) + self.authentication_config = KubernetesProviderAuthConfig( + **self.config.authentication + ) def __create_k8s_client(self): """ @@ -85,7 +85,9 @@ def __create_k8s_client(self): client_configuration.host = self.authentication_config.api_server client_configuration.verify_ssl = not self.authentication_config.insecure - client_configuration.api_key = {"authorization": "Bearer " + self.authentication_config.token} + client_configuration.api_key = { + "authorization": "Bearer " + self.authentication_config.token + } return client.ApiClient(client_configuration) @@ -108,11 +110,21 @@ def validate_scopes(self): return scopes - def _notify(self, action: str, kind: str, object_name: str, namespace: str, labels: str, **kwargs): + def _notify( + self, + action: str, + kind: str, + object_name: str, + namespace: str, + labels: str, + **kwargs, + ): if labels is None: labels = [] if action == "rollout_restart": - self.__rollout_restart(kind=kind, name=object_name, namespace=namespace, labels=labels) + self.__rollout_restart( + kind=kind, name=object_name, namespace=namespace, labels=labels + ) elif action == "list_pods": self.__list_pods(namespace=namespace, labels=labels) else: @@ -120,16 +132,16 @@ def _notify(self, action: str, kind: str, object_name: str, namespace: str, labe def __rollout_restart(self, kind, name, namespace, labels): api_client = self.__create_k8s_client() - self.logger.info(f"Performing rollout restart for {kind} {name} using kubernetes provider") + self.logger.info( + f"Performing rollout restart for {kind} {name} using kubernetes provider" + ) now = datetime.datetime.now(datetime.timezone.utc) now = str(now.isoformat("T") + "Z") body = { - 'spec': { - 'template': { - 'metadata': { - 'annotations': { - 'kubectl.kubernetes.io/restartedAt': now - } + "spec": { + "template": { + "metadata": { + "annotations": {"kubectl.kubernetes.io/restartedAt": now} } } } @@ -137,24 +149,44 @@ def __rollout_restart(self, kind, name, namespace, labels): apps_v1 = client.AppsV1Api(api_client) try: if kind == "deployment": - deployment_list = apps_v1.list_namespaced_deployment(namespace=namespace, label_selector=labels) + deployment_list = apps_v1.list_namespaced_deployment( + namespace=namespace, label_selector=labels + ) if not deployment_list.items: - raise ValueError(f"Deployment with labels {labels} not found in namespace {namespace}") - apps_v1.patch_namespaced_deployment(name=name, namespace=namespace, body=body) + raise ValueError( + f"Deployment with labels {labels} not found in namespace {namespace}" + ) + apps_v1.patch_namespaced_deployment( + name=name, namespace=namespace, body=body + ) elif kind == "statefulset": - statefulset_list = apps_v1.list_namespaced_stateful_set(namespace=namespace, label_selector=labels) + statefulset_list = apps_v1.list_namespaced_stateful_set( + namespace=namespace, label_selector=labels + ) if not statefulset_list.items: - raise ValueError(f"StatefulSet with labels {labels} not found in namespace {namespace}") - apps_v1.patch_namespaced_stateful_set(name=name, namespace=namespace, body=body) + raise ValueError( + f"StatefulSet with labels {labels} not found in namespace {namespace}" + ) + apps_v1.patch_namespaced_stateful_set( + name=name, namespace=namespace, body=body + ) elif kind == "daemonset": - daemonset_list = apps_v1.list_namespaced_daemon_set(namespace=namespace, label_selector=labels) + daemonset_list = apps_v1.list_namespaced_daemon_set( + namespace=namespace, label_selector=labels + ) if not daemonset_list.items: - raise ValueError(f"DaemonSet with labels {labels} not found in namespace {namespace}") - apps_v1.patch_namespaced_daemon_set(name=name, namespace=namespace, body=body) + raise ValueError( + f"DaemonSet with labels {labels} not found in namespace {namespace}" + ) + apps_v1.patch_namespaced_daemon_set( + name=name, namespace=namespace, body=body + ) else: raise ValueError(f"Unsupported kind {kind} to perform rollout restart") except ApiException as e: - self.logger.error(f"Error performing rollout restart for {kind} {name}: {e}") + self.logger.error( + f"Error performing rollout restart for {kind} {name}: {e}" + ) raise Exception(f"Error performing rollout restart for {kind} {name}: {e}") self.logger.info(f"Successfully performed rollout restart for {kind} {name}") @@ -168,10 +200,16 @@ def __list_pods(self, namespace, labels): try: core_v1.list_namespaced_pod(namespace=namespace, label_selector=labels) except ApiException as e: - self.logger.error(f"Error listing pods in namespace {namespace} with labels {labels}: {e}") - raise Exception(f"Error listing pods in namespace {namespace} with labels {labels}: {e}") - - self.logger.info(f"Successfully listed pods in namespace {namespace} with labels {labels}") + self.logger.error( + f"Error listing pods in namespace {namespace} with labels {labels}: {e}" + ) + raise Exception( + f"Error listing pods in namespace {namespace} with labels {labels}: {e}" + ) + + self.logger.info( + f"Successfully listed pods in namespace {namespace} with labels {labels}" + ) if __name__ == "__main__": @@ -182,6 +220,7 @@ def __list_pods(self, namespace, labels): # Load environment variables import os + url = os.environ.get("KUBERNETES_URL") token = os.environ.get("KUBERNETES_TOKEN") insecure = os.environ.get("KUBERNETES_INSECURE", "false").lower() == "true" @@ -197,7 +236,11 @@ def __list_pods(self, namespace, labels): }, ) - kubernetes_provider = KubernetesProvider(context_manager, "kubernetes_keephq", config) + kubernetes_provider = KubernetesProvider( + context_manager, "kubernetes_keephq", config + ) - result = kubernetes_provider.notify("rollout_restart", "deployment", "nginx", "default", {"app": "nginx"}) + result = kubernetes_provider.notify( + "rollout_restart", "deployment", "nginx", "default", {"app": "nginx"} + ) print(result) diff --git a/keep/providers/linear_provider/linear_provider.py b/keep/providers/linear_provider/linear_provider.py index 804743579..a8d2801bb 100644 --- a/keep/providers/linear_provider/linear_provider.py +++ b/keep/providers/linear_provider/linear_provider.py @@ -27,6 +27,7 @@ class LinearProvider(BaseProvider): PROVIDER_DISPLAY_NAME = "Linear" LINEAR_GRAPHQL_URL = "https://api.linear.app/graphql" + PROVIDER_CATEGORY = ["Ticketing"] def __init__( self, context_manager: ContextManager, provider_id: str, config: ProviderConfig diff --git a/keep/providers/linearb_provider/linearb_provider.py b/keep/providers/linearb_provider/linearb_provider.py index c04fce664..b3512c652 100644 --- a/keep/providers/linearb_provider/linearb_provider.py +++ b/keep/providers/linearb_provider/linearb_provider.py @@ -30,6 +30,7 @@ class LinearbProvider(BaseProvider): PROVIDER_DISPLAY_NAME = "LinearB" LINEARB_API = "https://public-api.linearb.io" + PROVIDER_CATEGORY = ["Developer Tools"] PROVIDER_SCOPES = [ ProviderScope( name="any", description="A way to validate the provider", mandatory=True diff --git a/keep/providers/mailchimp_provider/mailchimp_provider.py b/keep/providers/mailchimp_provider/mailchimp_provider.py index 8e17d1d8a..8a69d2ade 100644 --- a/keep/providers/mailchimp_provider/mailchimp_provider.py +++ b/keep/providers/mailchimp_provider/mailchimp_provider.py @@ -1,16 +1,16 @@ """ MailchimpProvider is a class that implements the Mailchimp API and allows email sending through Keep. """ + import dataclasses import pydantic +from mailchimp_transactional import Client from keep.contextmanager.contextmanager import ContextManager from keep.providers.base.base_provider import BaseProvider from keep.providers.models.provider_config import ProviderConfig, ProviderScope -from mailchimp_transactional import Client - @pydantic.dataclasses.dataclass class MailchimpProviderAuthConfig: @@ -27,6 +27,8 @@ class MailchimpProviderAuthConfig: class MailchimpProvider(BaseProvider): """Send email using the Mailchimp API.""" + PROVIDER_CATEGORY = ["Collaboration"] + PROVIDER_DISPLAY_NAME = "Mailchimp" PROVIDER_SCOPES = [ ProviderScope( @@ -38,7 +40,7 @@ class MailchimpProvider(BaseProvider): ] def __init__( - self, context_manager: ContextManager, provider_id: str, config: ProviderConfig + self, context_manager: ContextManager, provider_id: str, config: ProviderConfig ): super().__init__(context_manager, provider_id, config) @@ -90,19 +92,19 @@ def _notify(self, _from: str, to: str, subject: str, html: str, **kwargs) -> dic ) client = self.__generate_client() - res = client.messages.send({ - "message": {"from_email": _from, - "subject": subject, - "text": html, - "to": [{ - "email": to, - "type": "to" - }] - } - }) + res = client.messages.send( + { + "message": { + "from_email": _from, + "subject": subject, + "text": html, + "to": [{"email": to, "type": "to"}], + } + } + ) print(res) - if res[0]["status"] != 'sent': - error = res[0]['reject_reason'] + if res[0]["status"] != "sent": + error = res[0]["reject_reason"] raise Exception("Failed to send email: " + error) return res[0] @@ -125,7 +127,9 @@ def dispose(self): config = ProviderConfig( authentication={"api_key": mailchimp_api_key}, ) - provider = MailchimpProvider(context_manager, provider_id="mailchimp-test", config=config) + provider = MailchimpProvider( + context_manager, provider_id="mailchimp-test", config=config + ) response = provider.notify( "onboarding@mailchimp.dev", "youremail@gmail.com", diff --git a/keep/providers/mailgun_provider/mailgun_provider.py b/keep/providers/mailgun_provider/mailgun_provider.py index 36b8a5717..84c9879b7 100644 --- a/keep/providers/mailgun_provider/mailgun_provider.py +++ b/keep/providers/mailgun_provider/mailgun_provider.py @@ -48,6 +48,7 @@ class MailgunProviderAuthConfig: class MailgunProvider(BaseProvider): MAILGUN_API_KEY = os.environ.get("MAILGUN_API_KEY") WEBHOOK_INSTALLATION_REQUIRED = True + PROVIDER_CATEGORY = ["Collaboration"] def __init__( self, context_manager: ContextManager, provider_id: str, config: ProviderConfig diff --git a/keep/providers/mattermost_provider/mattermost_provider.py b/keep/providers/mattermost_provider/mattermost_provider.py index 4a2d6b312..aa2dd9298 100644 --- a/keep/providers/mattermost_provider/mattermost_provider.py +++ b/keep/providers/mattermost_provider/mattermost_provider.py @@ -26,6 +26,7 @@ class MattermostProvider(BaseProvider): """send alert message to Mattermost.""" PROVIDER_DISPLAY_NAME = "Mattermost" + PROVIDER_CATEGORY = ["Collaboration"] def __init__( self, context_manager: ContextManager, provider_id: str, config: ProviderConfig @@ -59,7 +60,7 @@ def _notify(self, message="", blocks=[], channel="", **kwargs: dict): webhook_url = self.authentication_config.webhook_url payload = {"text": message, "blocks": blocks} # channel is currently bugged (and unnecessary, as a webhook url is already one per channel) and so it is ignored for now - #if channel: + # if channel: # payload["channel"] = channel response = requests.post(webhook_url, json=payload, verify=False) diff --git a/keep/providers/microsoft-planner-provider/microsoft-planner-provider.py b/keep/providers/microsoft-planner-provider/microsoft-planner-provider.py index 7612e1f97..c95302c07 100644 --- a/keep/providers/microsoft-planner-provider/microsoft-planner-provider.py +++ b/keep/providers/microsoft-planner-provider/microsoft-planner-provider.py @@ -47,6 +47,8 @@ class PlannerProvider(BaseProvider): MS_PLANS_URL = urljoin(base=MS_GRAPH_BASE_URL, url="planner/plans") MS_TASKS_URL = urljoin(base=MS_GRAPH_BASE_URL, url="planner/tasks") + PROVIDER_CATEGORY = ["Collaboration"] + def __init__( self, context_manager: ContextManager, provider_id: str, config: ProviderConfig ): diff --git a/keep/providers/mongodb_provider/mongodb_provider.py b/keep/providers/mongodb_provider/mongodb_provider.py index 50bd7d939..c8c5b61b2 100644 --- a/keep/providers/mongodb_provider/mongodb_provider.py +++ b/keep/providers/mongodb_provider/mongodb_provider.py @@ -55,6 +55,7 @@ class MongodbProvider(BaseProvider): """Enrich alerts with data from MongoDB.""" PROVIDER_DISPLAY_NAME = "MongoDB" + PROVIDER_CATEGORY = ["Database"] PROVIDER_SCOPES = [ ProviderScope( @@ -77,7 +78,9 @@ def validate_scopes(self): """ try: client = self.__generate_client() - client.admin.command('ping') # will raise an exception if the server is not available + client.admin.command( + "ping" + ) # will raise an exception if the server is not available client.close() scopes = { "connect_to_server": True, @@ -118,7 +121,9 @@ def __generate_client(self): and k != "additional_options" # additional_options will go seperately and k != "database" } # database is not a valid mongo option - client = MongoClient(**client_conf, **additional_options, serverSelectionTimeoutMS=10000) # 10 seconds timeout + client = MongoClient( + **client_conf, **additional_options, serverSelectionTimeoutMS=10000 + ) # 10 seconds timeout return client def dispose(self): diff --git a/keep/providers/mysql_provider/mysql_provider.py b/keep/providers/mysql_provider/mysql_provider.py index 5ace58a75..1c2da6199 100644 --- a/keep/providers/mysql_provider/mysql_provider.py +++ b/keep/providers/mysql_provider/mysql_provider.py @@ -33,6 +33,7 @@ class MysqlProvider(BaseProvider): """Enrich alerts with data from MySQL.""" PROVIDER_DISPLAY_NAME = "MySQL" + PROVIDER_CATEGORY = ["Database"] PROVIDER_SCOPES = [ ProviderScope( diff --git a/keep/providers/netdata_provider/netdata_provider.py b/keep/providers/netdata_provider/netdata_provider.py index 114ef40e8..883c20ec6 100644 --- a/keep/providers/netdata_provider/netdata_provider.py +++ b/keep/providers/netdata_provider/netdata_provider.py @@ -45,6 +45,7 @@ class NetdataProvider(BaseProvider): PROVIDER_DISPLAY_NAME = "Netdata" PROVIDER_TAGS = ["alert"] + PROVIDER_CATEGORY = ["Monitoring"] def __init__( self, context_manager: ContextManager, provider_id: str, config: ProviderConfig diff --git a/keep/providers/newrelic_provider/newrelic_provider.py b/keep/providers/newrelic_provider/newrelic_provider.py index 49ecaa52b..82b11150d 100644 --- a/keep/providers/newrelic_provider/newrelic_provider.py +++ b/keep/providers/newrelic_provider/newrelic_provider.py @@ -49,6 +49,7 @@ class NewrelicProviderAuthConfig: class NewrelicProvider(BaseProvider): """Get alerts from New Relic into Keep.""" + PROVIDER_CATEGORY = ["Monitoring"] NEWRELIC_WEBHOOK_NAME = "keep-webhook" PROVIDER_DISPLAY_NAME = "New Relic" PROVIDER_SCOPES = [ diff --git a/keep/providers/ntfy_provider/ntfy_provider.py b/keep/providers/ntfy_provider/ntfy_provider.py index 992f750cb..fbcce89fb 100644 --- a/keep/providers/ntfy_provider/ntfy_provider.py +++ b/keep/providers/ntfy_provider/ntfy_provider.py @@ -61,6 +61,7 @@ class NtfyProviderAuthConfig: class NtfyProvider(BaseProvider): PROVIDER_DISPLAY_NAME = "Ntfy.sh" + PROVIDER_CATEGORY = ["Collaboration"] PROVIDER_SCOPES = [ ProviderScope( diff --git a/keep/providers/openai_provider/openai_provider.py b/keep/providers/openai_provider/openai_provider.py index ed107fbed..261e618e5 100644 --- a/keep/providers/openai_provider/openai_provider.py +++ b/keep/providers/openai_provider/openai_provider.py @@ -29,6 +29,7 @@ class OpenaiProviderAuthConfig: class OpenaiProvider(BaseProvider): PROVIDER_DISPLAY_NAME = "OpenAI" + PROVIDER_CATEGORY = ["Developer Tools"] def __init__( self, context_manager: ContextManager, provider_id: str, config: ProviderConfig diff --git a/keep/providers/openobserve_provider/openobserve_provider.py b/keep/providers/openobserve_provider/openobserve_provider.py index ca65b2a08..013801b63 100644 --- a/keep/providers/openobserve_provider/openobserve_provider.py +++ b/keep/providers/openobserve_provider/openobserve_provider.py @@ -73,7 +73,7 @@ class OpenobserveProvider(BaseProvider): """Install Webhooks and receive alerts from OpenObserve.""" PROVIDER_DISPLAY_NAME = "OpenObserve" - + PROVIDER_CATEGORY = ["Monitoring"] PROVIDER_SCOPES = [ ProviderScope( name="authenticated", diff --git a/keep/providers/openshift_provider/openshift_provider.py b/keep/providers/openshift_provider/openshift_provider.py index acfd90f6f..ad03bfb40 100644 --- a/keep/providers/openshift_provider/openshift_provider.py +++ b/keep/providers/openshift_provider/openshift_provider.py @@ -1,9 +1,10 @@ -import pydantic -import openshift_client as oc -from openshift_client import OpenShiftPythonException, Context import dataclasses import traceback +import openshift_client as oc +import pydantic +from openshift_client import Context, OpenShiftPythonException + from keep.contextmanager.contextmanager import ContextManager from keep.providers.base.base_provider import BaseProvider from keep.providers.models.provider_config import ProviderConfig, ProviderScope @@ -47,6 +48,7 @@ class OpenshiftProvider(BaseProvider): provider_id: str PROVIDER_DISPLAY_NAME = "Openshift" + PROVIDER_CATEGORY = ["Cloud Infrastructure"] PROVIDER_SCOPES = [ ProviderScope( @@ -57,9 +59,7 @@ class OpenshiftProvider(BaseProvider): ) ] - def __init__( - self, context_manager, provider_id: str, config: ProviderConfig - ): + def __init__(self, context_manager, provider_id: str, config: ProviderConfig): super().__init__(context_manager, provider_id, config) self.authentication_config = None self.validate_config() @@ -96,10 +96,12 @@ def validate_scopes(self): with oc.timeout(60 * 30), oc.tracking() as t, client: if oc.get_config_context() is None: try: - oc.invoke('login') + oc.invoke("login") except OpenShiftPythonException: traceback.print_exc() - self.logger.error(f'Tracking:\n{t.get_result().as_json(redact_streams=False)}\n\n') + self.logger.error( + f"Tracking:\n{t.get_result().as_json(redact_streams=False)}\n\n" + ) self.logger.error("Error logging into the API server") raise Exception("Error logging into the API server") scopes = { @@ -116,19 +118,25 @@ def _notify(self, kind: str, name: str, project_name: str): """Rollout restart the specified kind.""" client = self.__get_ocp_client() client.project_name = project_name - self.logger.info(f"Performing rollout restart for {kind} {name} using openshift provider") + self.logger.info( + f"Performing rollout restart for {kind} {name} using openshift provider" + ) with oc.timeout(60 * 30), oc.tracking() as t, client: if oc.get_config_context() is None: - self.logger.error(f'Current context not set! Logging into API server: {client.api_server}\n') + self.logger.error( + f"Current context not set! Logging into API server: {client.api_server}\n" + ) try: - oc.invoke('login') + oc.invoke("login") except OpenShiftPythonException: - self.logger.error('error occurred logging into API Server') + self.logger.error("error occurred logging into API Server") traceback.print_exc() - self.logger.error(f'Tracking:\n{t.get_result().as_json(redact_streams=False)}\n\n') + self.logger.error( + f"Tracking:\n{t.get_result().as_json(redact_streams=False)}\n\n" + ) raise Exception("Error logging into the API server") try: - oc.invoke('rollout', ['restart', kind, name]) + oc.invoke("rollout", ["restart", kind, name]) except OpenShiftPythonException: self.logger.error(f"Error restarting {kind} {name}") raise Exception(f"Error restarting {kind} {name}") diff --git a/keep/providers/opsgenie_provider/opsgenie_provider.py b/keep/providers/opsgenie_provider/opsgenie_provider.py index 231673dd5..36294641e 100644 --- a/keep/providers/opsgenie_provider/opsgenie_provider.py +++ b/keep/providers/opsgenie_provider/opsgenie_provider.py @@ -32,6 +32,7 @@ class OpsgenieProvider(BaseProvider): """Create incidents in OpsGenie.""" PROVIDER_DISPLAY_NAME = "OpsGenie" + PROVIDER_CATEGORY = ["Incident Management"] PROVIDER_SCOPES = [ ProviderScope( diff --git a/keep/providers/pagerduty_provider/pagerduty_provider.py b/keep/providers/pagerduty_provider/pagerduty_provider.py index 598cf091b..2118a2a9c 100644 --- a/keep/providers/pagerduty_provider/pagerduty_provider.py +++ b/keep/providers/pagerduty_provider/pagerduty_provider.py @@ -130,7 +130,7 @@ class PagerdutyProvider(BaseTopologyProvider, BaseIncidentProvider): if PAGERDUTY_CLIENT_ID is not None and PAGERDUTY_CLIENT_SECRET is not None else None ) - + PROVIDER_CATEGORY = ["Incident Management"] FINGERPRINT_FIELDS = ["alert_key"] def __init__( diff --git a/keep/providers/pagertree_provider/pagertree_provider.py b/keep/providers/pagertree_provider/pagertree_provider.py index dd900fda5..64cd97108 100644 --- a/keep/providers/pagertree_provider/pagertree_provider.py +++ b/keep/providers/pagertree_provider/pagertree_provider.py @@ -30,6 +30,7 @@ class PagertreeProvider(BaseProvider): """Get all alerts from pagertree""" PROVIDER_DISPLAY_NAME = "PagerTree" + PROVIDER_CATEGORY = ["Incident Management"] PROVIDER_SCOPES = [ ProviderScope( @@ -41,14 +42,14 @@ class PagertreeProvider(BaseProvider): ] def __init__( - self, context_manager: ContextManager, provider_id: str, config: ProviderConfig + self, context_manager: ContextManager, provider_id: str, config: ProviderConfig ): super().__init__(context_manager, provider_id, config) def __get_headers(self): return { - 'Accept': 'application/json', - 'Authorization': f'Bearer {self.authentication_config.api_token}', + "Accept": "application/json", + "Authorization": f"Bearer {self.authentication_config.api_token}", } def validate_scopes(self): @@ -56,7 +57,9 @@ def validate_scopes(self): Validates that the user has the required scopes to use the provider. """ try: - response = requests.get('https://api.pagertree.com/api/v4/alerts', headers=self.__get_headers()) + response = requests.get( + "https://api.pagertree.com/api/v4/alerts", headers=self.__get_headers() + ) if response.status_code == 200: scopes = { @@ -87,33 +90,42 @@ def validate_config(self): def _get_alerts(self) -> list[AlertDto]: try: - response = requests.get('https://api.pagertree.com/api/v4/alerts', headers=self.__get_headers()) + response = requests.get( + "https://api.pagertree.com/api/v4/alerts", headers=self.__get_headers() + ) if not response.ok: self.logger.error("Failed to get alerts", extra=response.json()) raise Exception("Could not get alerts") - return [AlertDto( - id=alert["id"], - status=alert["status"], - severity=alert["urgency"], - source=alert["source"], - message=alert["title"], - startedAt=alert["created_at"], - description=alert["description"] - ) for alert in response.json()['alerts']] + return [ + AlertDto( + id=alert["id"], + status=alert["status"], + severity=alert["urgency"], + source=alert["source"], + message=alert["title"], + startedAt=alert["created_at"], + description=alert["description"], + ) + for alert in response.json()["alerts"] + ] except Exception as e: - self.logger.error("Error while getting PagerTree alerts", extra={"error": str(e)}) + self.logger.error( + "Error while getting PagerTree alerts", extra={"error": str(e)} + ) raise e - def __send_alert(self, - title: str, - description: str, - urgency: Literal["low", "medium", "high", "critical"], - destination_team_ids: list[str], - destination_router_ids: list[str], - destination_account_user_ids: list[str], - status: Literal["queued", "open", "acknowledged", "resolved", "dropped"], - **kwargs: dict, ): + def __send_alert( + self, + title: str, + description: str, + urgency: Literal["low", "medium", "high", "critical"], + destination_team_ids: list[str], + destination_router_ids: list[str], + destination_account_user_ids: list[str], + status: Literal["queued", "open", "acknowledged", "resolved", "dropped"], + **kwargs: dict, + ): """ Sends PagerDuty Alert @@ -126,29 +138,36 @@ def __send_alert(self, destination_account_user_ids: destination account_users_ids to send alert to status: alert status to send """ - response = requests.post('https://api.pagertree.com/api/v4/alerts', headers=self.__get_headers(), data={ - "title": title, - "description": description, - "urgency": urgency, - "destination_team_ids": destination_team_ids, - "destination_router_ids": destination_router_ids, - "destination_account_user_ids": destination_account_user_ids, - "status": status, - **kwargs - }) + response = requests.post( + "https://api.pagertree.com/api/v4/alerts", + headers=self.__get_headers(), + data={ + "title": title, + "description": description, + "urgency": urgency, + "destination_team_ids": destination_team_ids, + "destination_router_ids": destination_router_ids, + "destination_account_user_ids": destination_account_user_ids, + "status": status, + **kwargs, + }, + ) if not response.ok: self.logger.error("Failed to send alert", extra={"error": response.json()}) self.logger.info("Alert status: %s", response.status_code) self.logger.info("Alert created successfully", response.json()) - def __send_incident(self, title: str, - incident_severity: str, - incident_message: str, - urgency: Literal["low", "medium", "high", "critical"], - destination_team_ids: list[str], - destination_router_ids: list[str], - destination_account_user_ids: list[str], - **kwargs: dict, ): + def __send_incident( + self, + title: str, + incident_severity: str, + incident_message: str, + urgency: Literal["low", "medium", "high", "critical"], + destination_team_ids: list[str], + destination_router_ids: list[str], + destination_account_user_ids: list[str], + **kwargs: dict, + ): """ Marking an alert as an incident communicates to your team members this alert is a greater degree of severity than a normal alert. @@ -161,42 +180,76 @@ def __send_incident(self, title: str, destination_account_user_ids: destination account_users_ids to send alert to """ - response = requests.post('https://api.pagertree.com/api/v4/alerts', headers=self.__get_headers(), data={ - "title": title, - "meta": { - "incident": True, - "incident_severity": incident_severity, - "incident_message": incident_message + response = requests.post( + "https://api.pagertree.com/api/v4/alerts", + headers=self.__get_headers(), + data={ + "title": title, + "meta": { + "incident": True, + "incident_severity": incident_severity, + "incident_message": incident_message, + }, + "urgency": urgency, + "destination_team_ids": destination_team_ids, + "destination_router_ids": destination_router_ids, + "destination_account_user_ids": destination_account_user_ids, + **kwargs, }, - "urgency": urgency, - "destination_team_ids": destination_team_ids, - "destination_router_ids": destination_router_ids, - "destination_account_user_ids": destination_account_user_ids, - **kwargs - }) + ) if not response.ok: - self.logger.error("Failed to send incident", extra={"error": response.json()}) + self.logger.error( + "Failed to send incident", extra={"error": response.json()} + ) self.logger.info("Incident status: %s", response.status_code) self.logger.info("Incident created successfully", response.json()) - def _notify(self, - title: str, - urgency: Literal["low", "medium", "high", "critical"], - incident: bool = False, - severities: Literal["SEV-1", "SEV-2", "SEV-3", "SEV-4", "SEV-5", "SEV_UNKNOWN"] = "SEV-5", - incident_message: str = "", - description: str = "", - status: Literal["queued", "open", "acknowledged", "resolved", "dropped"] = "queued", - destination_team_ids: list[str] = [], - destination_router_ids: list[str] = [], - destination_account_user_ids: list[str] = [], - **kwargs: dict, ): - if len(destination_team_ids) + len(destination_router_ids) + len(destination_account_user_ids) == 0: - raise Exception("at least 1 destination (Team, Router, or Account User) is required") + def _notify( + self, + title: str, + urgency: Literal["low", "medium", "high", "critical"], + incident: bool = False, + severities: Literal[ + "SEV-1", "SEV-2", "SEV-3", "SEV-4", "SEV-5", "SEV_UNKNOWN" + ] = "SEV-5", + incident_message: str = "", + description: str = "", + status: Literal[ + "queued", "open", "acknowledged", "resolved", "dropped" + ] = "queued", + destination_team_ids: list[str] = [], + destination_router_ids: list[str] = [], + destination_account_user_ids: list[str] = [], + **kwargs: dict, + ): + if ( + len(destination_team_ids) + + len(destination_router_ids) + + len(destination_account_user_ids) + == 0 + ): + raise Exception( + "at least 1 destination (Team, Router, or Account User) is required" + ) if not incident: - self.__send_alert(title, description, urgency, destination_team_ids, destination_router_ids, - destination_account_user_ids, status, **kwargs) + self.__send_alert( + title, + description, + urgency, + destination_team_ids, + destination_router_ids, + destination_account_user_ids, + status, + **kwargs, + ) else: - self.__send_incident(incident_message, severities, title, urgency, destination_team_ids, - destination_router_ids, - destination_account_user_ids, **kwargs) + self.__send_incident( + incident_message, + severities, + title, + urgency, + destination_team_ids, + destination_router_ids, + destination_account_user_ids, + **kwargs, + ) diff --git a/keep/providers/parseable_provider/parseable_provider.py b/keep/providers/parseable_provider/parseable_provider.py index ef6980771..70d5caee8 100644 --- a/keep/providers/parseable_provider/parseable_provider.py +++ b/keep/providers/parseable_provider/parseable_provider.py @@ -50,6 +50,7 @@ class ParseableProviderAuthConfig: class ParseableProvider(BaseProvider): """Parseable provider to ingest data from Parseable.""" + PROVIDER_CATEGORY = ["Monitoring"] webhook_description = "This is an example of how to configure an alert to be sent to Keep using Parseable's webhook feature. Post this to https://YOUR_PARSEABLE_SERVER/api/v1/logstream/YOUR_STREAM_NAME/alert" webhook_template = """{{ "version": "v1", diff --git a/keep/providers/pingdom_provider/pingdom_provider.py b/keep/providers/pingdom_provider/pingdom_provider.py index 87b27dde3..62861df92 100644 --- a/keep/providers/pingdom_provider/pingdom_provider.py +++ b/keep/providers/pingdom_provider/pingdom_provider.py @@ -34,7 +34,7 @@ class PingdomProvider(BaseProvider): 4. Click Save Integration. """ webhook_template = """""" - + PROVIDER_CATEGORY = ["Monitoring"] PROVIDER_SCOPES = [ ProviderScope( name="read", diff --git a/keep/providers/postgres_provider/postgres_provider.py b/keep/providers/postgres_provider/postgres_provider.py index 1191f80db..0290a554c 100644 --- a/keep/providers/postgres_provider/postgres_provider.py +++ b/keep/providers/postgres_provider/postgres_provider.py @@ -41,6 +41,7 @@ class PostgresProvider(BaseProvider): """Enrich alerts with data from Postgres.""" PROVIDER_DISPLAY_NAME = "PostgreSQL" + PROVIDER_CATEGORY = ["Database"] PROVIDER_SCOPES = [ ProviderScope( name="connect_to_server", @@ -104,11 +105,7 @@ def validate_config(self): **self.config.authentication ) - def _query( - self, - query: str, - **kwargs: dict - ) -> list | tuple: + def _query(self, query: str, **kwargs: dict) -> list | tuple: """ Executes a query against the Postgres database. @@ -135,11 +132,7 @@ def _query( # Close the database connection conn.close() - def _notify( - self, - query: str, - **kwargs - ): + def _notify(self, query: str, **kwargs): """ Notifies the Postgres database. """ diff --git a/keep/providers/prometheus_provider/prometheus_provider.py b/keep/providers/prometheus_provider/prometheus_provider.py index 19d57f9df..5f4d96d40 100644 --- a/keep/providers/prometheus_provider/prometheus_provider.py +++ b/keep/providers/prometheus_provider/prometheus_provider.py @@ -70,6 +70,7 @@ class PrometheusProvider(BaseProvider): "info": AlertSeverity.INFO, "low": AlertSeverity.LOW, } + PROVIDER_CATEGORY = ["Monitoring"] STATUS_MAP = { "firing": AlertStatus.FIRING, diff --git a/keep/providers/providers_factory.py b/keep/providers/providers_factory.py index b233d0354..16bffd92c 100644 --- a/keep/providers/providers_factory.py +++ b/keep/providers/providers_factory.py @@ -401,6 +401,8 @@ def get_all_providers(ignore_cache_file: bool = False) -> list[Provider]: tags=provider_tags, alertExample=alert_example, default_fingerprint_fields=default_fingerprint_fields, + categories=provider_class.PROVIDER_CATEGORY, + coming_soon=provider_class.PROVIDER_COMING_SOON, ) ) except ModuleNotFoundError: diff --git a/keep/providers/pushover_provider/pushover_provider.py b/keep/providers/pushover_provider/pushover_provider.py index dd4bda525..b67e9d8bb 100644 --- a/keep/providers/pushover_provider/pushover_provider.py +++ b/keep/providers/pushover_provider/pushover_provider.py @@ -28,6 +28,7 @@ class PushoverProvider(BaseProvider): """Send alert message to Pushover.""" PROVIDER_DISPLAY_NAME = "Pushover" + PROVIDER_CATEGORY = ["Collaboration"] def __init__( self, context_manager: ContextManager, provider_id: str, config: ProviderConfig diff --git a/keep/providers/quickchart_provider/quickchart_provider.py b/keep/providers/quickchart_provider/quickchart_provider.py index 2b19b1764..b72bf66c2 100644 --- a/keep/providers/quickchart_provider/quickchart_provider.py +++ b/keep/providers/quickchart_provider/quickchart_provider.py @@ -42,6 +42,7 @@ class QuickchartProviderAuthConfig: class QuickchartProvider(BaseProvider): PROVIDER_DISPLAY_NAME = "QuickChart" + PROVIDER_CATEGORY = ["Developer Tools"] def __init__( self, context_manager: ContextManager, provider_id: str, config: ProviderConfig diff --git a/keep/providers/redmine_provider/redmine_provider.py b/keep/providers/redmine_provider/redmine_provider.py index 27150f3e4..7364e8448 100644 --- a/keep/providers/redmine_provider/redmine_provider.py +++ b/keep/providers/redmine_provider/redmine_provider.py @@ -49,9 +49,10 @@ class RedmineProvider(BaseProvider): ), ] PROVIDER_TAGS = ["ticketing"] + PROVIDER_CATEGORY = ["Ticketing"] def __init__( - self, context_manager: ContextManager, provider_id: str, config: ProviderConfig + self, context_manager: ContextManager, provider_id: str, config: ProviderConfig ): self._host = None super().__init__(context_manager, provider_id, config) @@ -69,16 +70,23 @@ def validate_scopes(self): try: resp.raise_for_status() if resp.status_code == 200: - scopes = { - "authenticated": True - } + scopes = {"authenticated": True} else: - self.logger.error(f"Failed to validate scope for {self.provider_id}", extra=resp.json()) + self.logger.error( + f"Failed to validate scope for {self.provider_id}", + extra=resp.json(), + ) scopes = { - "authenticated": {"status_code": resp.status_code, "error": resp.json()} + "authenticated": { + "status_code": resp.status_code, + "error": resp.json(), + } } except HTTPError as e: - self.logger.error(f"HTTPError while validating scope for {self.provider_id}", extra={"error": str(e)}) + self.logger.error( + f"HTTPError while validating scope for {self.provider_id}", + extra={"error": str(e)}, + ) scopes = { "authenticated": {"status_code": resp.status_code, "error": str(e)} } @@ -98,7 +106,7 @@ def __redmine_url(self): # if the user explicitly supplied a host with http/https, use it if self.authentication_config.host.startswith( - "http://" + "http://" ) or self.authentication_config.host.startswith("https://"): self._host = self.authentication_config.host return self.authentication_config.host.rstrip("/") @@ -144,18 +152,36 @@ def __build_payload_from_kwargs(self, kwargs: dict): params[param] = kwargs[param] return params - def _notify(self, project_id: str, subject: str, priority_id: str, description: str = "", - **kwargs: dict): + def _notify( + self, + project_id: str, + subject: str, + priority_id: str, + description: str = "", + **kwargs: dict, + ): self.logger.info("Creating an issue in redmine") payload = self.__build_payload_from_kwargs( - kwargs={**kwargs, 'subject': subject, 'description': description, "project_id": project_id, - "priority_id": priority_id}) - resp = requests.post(f"{self.__redmine_url}/issues.json", headers=self.__get_headers(), - json={'issue': payload}) + kwargs={ + **kwargs, + "subject": subject, + "description": description, + "project_id": project_id, + "priority_id": priority_id, + } + ) + resp = requests.post( + f"{self.__redmine_url}/issues.json", + headers=self.__get_headers(), + json={"issue": payload}, + ) try: resp.raise_for_status() except HTTPError as e: self.logger.error("Error While creating Redmine Issue") raise Exception(f"Failed to create issue: {str(e)}") - self.logger.info("Successfully created a Redmine Issue", extra={"status_code": resp.status_code}) + self.logger.info( + "Successfully created a Redmine Issue", + extra={"status_code": resp.status_code}, + ) return resp.json() diff --git a/keep/providers/resend_provider/resend_provider.py b/keep/providers/resend_provider/resend_provider.py index 2a7734e39..9509f6152 100644 --- a/keep/providers/resend_provider/resend_provider.py +++ b/keep/providers/resend_provider/resend_provider.py @@ -1,6 +1,7 @@ """ ResendProvider is a class that implements the Resend API and allows email sending through Keep. """ + import dataclasses import pydantic @@ -27,6 +28,7 @@ class ResendProvider(BaseProvider): """Send email using the Resend API.""" PROVIDER_DISPLAY_NAME = "Resend" + PROVIDER_CATEGORY = ["Collaboration"] RESEND_API_URL = "https://api.resend.com" diff --git a/keep/providers/rollbar_provider/rollbar_provider.py b/keep/providers/rollbar_provider/rollbar_provider.py index 41b6d9007..c0fa39c54 100644 --- a/keep/providers/rollbar_provider/rollbar_provider.py +++ b/keep/providers/rollbar_provider/rollbar_provider.py @@ -35,7 +35,7 @@ class RollbarProviderAuthConfig: class RollbarProvider(BaseProvider): PROVIDER_DISPLAY_NAME = "Rollbar" PROVIDER_TAGS = ["alert"] - + PROVIDER_CATEGORY = ["Monitoring"] PROVIDER_SCOPES = [ ProviderScope( name="authenticated", diff --git a/keep/providers/salesforce_provider/__init__.py b/keep/providers/salesforce_provider/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/keep/providers/salesforce_provider/salesforce_provider.py b/keep/providers/salesforce_provider/salesforce_provider.py new file mode 100644 index 000000000..f1d252de3 --- /dev/null +++ b/keep/providers/salesforce_provider/salesforce_provider.py @@ -0,0 +1,36 @@ +import dataclasses + +import pydantic + +from keep.contextmanager.contextmanager import ContextManager +from keep.providers.base.base_provider import BaseProvider +from keep.providers.models.provider_config import ProviderConfig + + +@pydantic.dataclasses.dataclass +class SalesforceProviderAuthConfig: + api_key: str = dataclasses.field( + metadata={"required": True, "description": "Zendesk API key", "sensitive": True} + ) + + +class SalesforceProvider(BaseProvider): + PROVIDER_DISPLAY_NAME = "Salesforce" + PROVIDER_CATEGORY = ["CRM"] + PROVIDER_COMING_SOON = True + + 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 = SalesforceProviderAuthConfig( + **self.config.authentication + ) + + def dispose(self): + """ + No need to dispose of anything, so just do nothing. + """ + pass diff --git a/keep/providers/sendgrid_provider/sendgrid_provider.py b/keep/providers/sendgrid_provider/sendgrid_provider.py index db14f0b12..3ed4e90b6 100644 --- a/keep/providers/sendgrid_provider/sendgrid_provider.py +++ b/keep/providers/sendgrid_provider/sendgrid_provider.py @@ -45,6 +45,7 @@ class SendgridProvider(BaseProvider): """Send email using the SendGrid API.""" PROVIDER_DISPLAY_NAME = "SendGrid" + PROVIDER_CATEGORY = ["Collaboration"] PROVIDER_SCOPES = [ ProviderScope( name="email.send", diff --git a/keep/providers/sentry_provider/sentry_provider.py b/keep/providers/sentry_provider/sentry_provider.py index 5a6d615d4..42c2693f0 100644 --- a/keep/providers/sentry_provider/sentry_provider.py +++ b/keep/providers/sentry_provider/sentry_provider.py @@ -77,7 +77,7 @@ class SentryProvider(BaseProvider): ), ] DEFAULT_TIMEOUT = 600 - + PROVIDER_CATEGORY = ["Monitoring"] SEVERITIES_MAP = { "fatal": AlertSeverity.CRITICAL, "error": AlertSeverity.HIGH, diff --git a/keep/providers/servicenow_provider/servicenow_provider.py b/keep/providers/servicenow_provider/servicenow_provider.py index 307dea496..d08c09f97 100644 --- a/keep/providers/servicenow_provider/servicenow_provider.py +++ b/keep/providers/servicenow_provider/servicenow_provider.py @@ -68,6 +68,7 @@ class ServicenowProviderAuthConfig: class ServicenowProvider(BaseTopologyProvider): """Manage ServiceNow tickets.""" + PROVIDER_CATEGORY = ["Ticketing"] PROVIDER_SCOPES = [ ProviderScope( name="itil", diff --git a/keep/providers/signalfx_provider/signalfx_provider.py b/keep/providers/signalfx_provider/signalfx_provider.py index 0b490bdd5..3987d70c3 100644 --- a/keep/providers/signalfx_provider/signalfx_provider.py +++ b/keep/providers/signalfx_provider/signalfx_provider.py @@ -74,6 +74,8 @@ class SignalfxProviderAuthConfig: class SignalfxProvider(BaseProvider): """Get alerts from SignalFx into Keep.""" + PROVIDER_CATEGORY = ["Monitoring"] + PROVIDER_SCOPES = [ ProviderScope( name="API", diff --git a/keep/providers/signl4_provider/signl4_provider.py b/keep/providers/signl4_provider/signl4_provider.py index 9897a3248..3bafdde1b 100644 --- a/keep/providers/signl4_provider/signl4_provider.py +++ b/keep/providers/signl4_provider/signl4_provider.py @@ -9,6 +9,7 @@ from keep.providers.models.provider_config import ProviderConfig, ProviderScope from keep.providers.providers_factory import ProvidersFactory + class S4Status(str, enum.Enum): """ SIGNL4 alert status. @@ -18,6 +19,7 @@ class S4Status(str, enum.Enum): ACKNOWLEDGED = "acknowledged" RESOLVED = "resolved" + class S4AlertingScenario(str, enum.Enum): """ SIGNL4 alerting scenario. @@ -44,6 +46,7 @@ class Signl4Provider(BaseProvider): """Trigger SIGNL4 alerts.""" PROVIDER_DISPLAY_NAME = "SIGNL4" + PROVIDER_CATEGORY = ["Incident Management"] PROVIDER_SCOPES = [ ProviderScope( @@ -88,7 +91,7 @@ def dispose(self): def _notify( self, title: str | None = None, - message: str | None = None, + message: str | None = None, user: str | None = None, s4_external_id: str | None = None, s4_status: S4Status = S4Status.NEW, @@ -108,37 +111,41 @@ def _notify( # Alert data alert_data = { - 'title': title, - 'message': message, - 'user': user, - 'X-S4-ExternalID': s4_external_id, - 'X-S4-Status': s4_status, - 'X-S4-Service': s4_service, - 'X-S4-Location': s4_location, - 'X-S4-AlertingScenario': s4_alerting_scenario, - 'X-S4-Filtering': s4_filtering, - 'X-S4-SourceSystem': 'Keep', + "title": title, + "message": message, + "user": user, + "X-S4-ExternalID": s4_external_id, + "X-S4-Status": s4_status, + "X-S4-Service": s4_service, + "X-S4-Location": s4_location, + "X-S4-AlertingScenario": s4_alerting_scenario, + "X-S4-Filtering": s4_filtering, + "X-S4-SourceSystem": "Keep", **kwargs, } # SIGNL4 webhook URL - webhook_url = 'https://connect.signl4.com/webhook/' + self.authentication_config.signl4_integration_secret + webhook_url = ( + "https://connect.signl4.com/webhook/" + + self.authentication_config.signl4_integration_secret + ) try: - result = requests.post(url = webhook_url, json = alert_data) + result = requests.post(url=webhook_url, json=alert_data) if result.status_code == 201: # Success self.logger.info(result.text) else: # Error - self.logger.exception('Error: ' + str(result.status_code)) - raise Exception('Error: ' + str(result.status_code)) + self.logger.exception("Error: " + str(result.status_code)) + raise Exception("Error: " + str(result.status_code)) except: self.logger.exception("Failed to create SIGNL4 alert") raise + if __name__ == "__main__": # Output debug messages import logging diff --git a/keep/providers/site24x7_provider/site24x7_provider.py b/keep/providers/site24x7_provider/site24x7_provider.py index c474155e5..d2fe50fbb 100644 --- a/keep/providers/site24x7_provider/site24x7_provider.py +++ b/keep/providers/site24x7_provider/site24x7_provider.py @@ -78,7 +78,7 @@ class Site24X7Provider(BaseProvider): alias="Valid TLD", ), ] - + PROVIDER_CATEGORY = ["Monitoring"] SEVERITIES_MAP = { "DOWN": AlertSeverity.WARNING, "TROUBLE": AlertSeverity.HIGH, diff --git a/keep/providers/slack_provider/slack_provider.py b/keep/providers/slack_provider/slack_provider.py index e1bef052e..84c44a850 100644 --- a/keep/providers/slack_provider/slack_provider.py +++ b/keep/providers/slack_provider/slack_provider.py @@ -46,6 +46,7 @@ class SlackProvider(BaseProvider): SLACK_CLIENT_ID = os.environ.get("SLACK_CLIENT_ID") SLACK_CLIENT_SECRET = os.environ.get("SLACK_CLIENT_SECRET") SLACK_API = "https://slack.com/api" + PROVIDER_CATEGORY = ["Collaboration"] def __init__( self, context_manager: ContextManager, provider_id: str, config: ProviderConfig diff --git a/keep/providers/smtp_provider/smtp_provider.py b/keep/providers/smtp_provider/smtp_provider.py index f8f187c09..3588da81c 100644 --- a/keep/providers/smtp_provider/smtp_provider.py +++ b/keep/providers/smtp_provider/smtp_provider.py @@ -73,6 +73,7 @@ class SmtpProvider(BaseProvider): alias="Send Email", ) ] + PROVIDER_CATEGORY = ["Collaboration"] PROVIDER_TAGS = ["messaging"] PROVIDER_DISPLAY_NAME = "SMTP" diff --git a/keep/providers/snowflake_provider/snowflake_provider.py b/keep/providers/snowflake_provider/snowflake_provider.py index edd10a8df..fd5fbd169 100644 --- a/keep/providers/snowflake_provider/snowflake_provider.py +++ b/keep/providers/snowflake_provider/snowflake_provider.py @@ -46,6 +46,7 @@ class SnowflakeProvider(BaseProvider): """Enrich alerts with data from Snowflake.""" PROVIDER_DISPLAY_NAME = "Snowflake" + PROVIDER_CATEGORY = ["Database"] def __init__( self, context_manager: ContextManager, provider_id: str, config: ProviderConfig diff --git a/keep/providers/splunk_provider/splunk_provider.py b/keep/providers/splunk_provider/splunk_provider.py index bbdfb7980..440f23b43 100644 --- a/keep/providers/splunk_provider/splunk_provider.py +++ b/keep/providers/splunk_provider/splunk_provider.py @@ -59,7 +59,7 @@ class SplunkProvider(BaseProvider): ), ] FINGERPRINT_FIELDS = ["exception", "logger", "service"] - + PROVIDER_CATEGORY = ["Monitoring"] SEVERITIES_MAP = { "LOW": AlertSeverity.LOW, "INFO": AlertSeverity.INFO, diff --git a/keep/providers/squadcast_provider/squadcast_provider.py b/keep/providers/squadcast_provider/squadcast_provider.py index 412b40d40..3a708f2e4 100644 --- a/keep/providers/squadcast_provider/squadcast_provider.py +++ b/keep/providers/squadcast_provider/squadcast_provider.py @@ -1,6 +1,7 @@ """ SquadcastProvider is a class that implements the Squadcast API and allows creating incidents and notes. """ + import dataclasses import json @@ -49,6 +50,7 @@ class SquadcastProvider(BaseProvider): PROVIDER_DISPLAY_NAME = "Squadcast" PROVIDER_TAGS = ["alert"] + PROVIDER_CATEGORY = ["Incident Management"] PROVIDER_SCOPES = [ ProviderScope( @@ -134,7 +136,7 @@ def _create_incidents( # append body to additional_json we are doing this way because we don't want to override the core body fields body = json.dumps({**json.loads(additional_json), **json.loads(body)}) - + return requests.post( self.authentication_config.webhook_url, data=body, headers=headers ) diff --git a/keep/providers/ssh_provider/ssh_provider.py b/keep/providers/ssh_provider/ssh_provider.py index e48f9d0ad..7b5f34eaf 100644 --- a/keep/providers/ssh_provider/ssh_provider.py +++ b/keep/providers/ssh_provider/ssh_provider.py @@ -63,6 +63,7 @@ class SshProvider(BaseProvider): """Enrich alerts with data from SSH.""" PROVIDER_DISPLAY_NAME = "SSH" + PROVIDER_CATEGORY = ["Cloud Infrastructure", "Developer Tools"] def __init__( self, context_manager: ContextManager, provider_id: str, config: ProviderConfig diff --git a/keep/providers/statuscake_provider/statuscake_provider.py b/keep/providers/statuscake_provider/statuscake_provider.py index ec526648d..9d3977ae7 100644 --- a/keep/providers/statuscake_provider/statuscake_provider.py +++ b/keep/providers/statuscake_provider/statuscake_provider.py @@ -4,7 +4,7 @@ import dataclasses from typing import List -from urllib.parse import urljoin, urlencode +from urllib.parse import urlencode, urljoin import pydantic import requests @@ -34,7 +34,7 @@ class StatuscakeProviderAuthConfig: class StatuscakeProvider(BaseProvider): PROVIDER_DISPLAY_NAME = "Statuscake" PROVIDER_TAGS = ["alert"] - + PROVIDER_CATEGORY = ["Monitoring"] PROVIDER_SCOPES = [ ProviderScope( name="alerts", @@ -229,7 +229,9 @@ def __update_alert(self, data: dict, paths: list): ) if not response.ok: raise Exception(response.text) - self.logger.info("Successfully updated alert", extra={"data": data, "paths": paths}) + self.logger.info( + "Successfully updated alert", extra={"data": data, "paths": paths} + ) except Exception as e: self.logger.error("Error while updating alert", extra={"exception": str(e)}) raise e @@ -349,25 +351,27 @@ def _format_alert( severity = AlertSeverity.HIGH alert = AlertDto( - id=event.get('TestID', event.get("Name")), + id=event.get("TestID", event.get("Name")), name=event.get("Name"), status=status if status is not None else AlertStatus.FIRING, severity=severity, url=event.get("URL", None), ip=event.get("IP", None), tags=event.get("Tags", None), - test_id=event.get('TestID', None), + test_id=event.get("TestID", None), method=event.get("Method", None), checkrate=event.get("Checkrate", None), status_code=event.get("StatusCode", None), source=["statuscake"], ) - alert.fingerprint = StatuscakeProvider.get_alert_fingerprint( - alert, - ( - StatuscakeProvider.FINGERPRINT_FIELDS - ), - ) if event.get("TestID", None) else None + alert.fingerprint = ( + StatuscakeProvider.get_alert_fingerprint( + alert, + (StatuscakeProvider.FINGERPRINT_FIELDS), + ) + if event.get("TestID", None) + else None + ) return alert diff --git a/keep/providers/sumologic_provider/sumologic_provider.py b/keep/providers/sumologic_provider/sumologic_provider.py index 9ffb79bc2..35ee79830 100644 --- a/keep/providers/sumologic_provider/sumologic_provider.py +++ b/keep/providers/sumologic_provider/sumologic_provider.py @@ -57,7 +57,7 @@ class SumologicProvider(BaseProvider): """Install Webhooks and receive alerts from SumoLogic.""" PROVIDER_DISPLAY_NAME = "SumoLogic" - + PROVIDER_CATEGORY = ["Monitoring"] PROVIDER_SCOPES = [ ProviderScope( name="authenticated", diff --git a/keep/providers/teams_provider/teams_provider.py b/keep/providers/teams_provider/teams_provider.py index 1749677e2..d7898e9a5 100644 --- a/keep/providers/teams_provider/teams_provider.py +++ b/keep/providers/teams_provider/teams_provider.py @@ -1,6 +1,7 @@ """ TeamsProvider is a class that implements the BaseOutputProvider interface for Microsoft Teams messages. """ + import dataclasses import pydantic @@ -28,7 +29,8 @@ class TeamsProviderAuthConfig: class TeamsProvider(BaseProvider): """Send alert message to Teams.""" - PROVIDER_DISPLAY_NAME = "Teams" + PROVIDER_DISPLAY_NAME = "Microsoft Teams" + PROVIDER_CATEGORY = ["Collaboration"] def __init__( self, context_manager: ContextManager, provider_id: str, config: ProviderConfig diff --git a/keep/providers/telegram_provider/telegram_provider.py b/keep/providers/telegram_provider/telegram_provider.py index c4e0dd04b..b69a1aeab 100644 --- a/keep/providers/telegram_provider/telegram_provider.py +++ b/keep/providers/telegram_provider/telegram_provider.py @@ -31,6 +31,7 @@ class TelegramProvider(BaseProvider): """Send alert message to Telegram.""" PROVIDER_DISPLAY_NAME = "Telegram" + PROVIDER_CATEGORY = ["Collaboration"] def __init__( self, context_manager: ContextManager, provider_id: str, config: ProviderConfig diff --git a/keep/providers/trello_provider/trello_provider.py b/keep/providers/trello_provider/trello_provider.py index 6f80e90c9..1d97bd8d1 100644 --- a/keep/providers/trello_provider/trello_provider.py +++ b/keep/providers/trello_provider/trello_provider.py @@ -1,6 +1,7 @@ """ TrelloOutput is a class that implements the BaseOutputProvider interface for Trello updates. """ + import dataclasses import pydantic @@ -32,6 +33,7 @@ class TrelloProvider(BaseProvider): """Enrich alerts with data from Trello.""" PROVIDER_DISPLAY_NAME = "Trello" + PROVIDER_CATEGORY = ["Collaboration"] def __init__( self, context_manager: ContextManager, provider_id: str, config: ProviderConfig @@ -49,12 +51,7 @@ def dispose(self): """ pass - def _query( - self, - board_id: str = "", - filter: str = "createCard", - **kwargs: dict - ): + def _query(self, board_id: str = "", filter: str = "createCard", **kwargs: dict): """ Notify alert message to Slack using the Slack Incoming Webhook API https://api.slack.com/messaging/webhooks diff --git a/keep/providers/twilio_provider/twilio_provider.py b/keep/providers/twilio_provider/twilio_provider.py index 3dea3ed9d..17c492730 100644 --- a/keep/providers/twilio_provider/twilio_provider.py +++ b/keep/providers/twilio_provider/twilio_provider.py @@ -1,6 +1,7 @@ """ TwilioProvider is a class that implements the BaseProvider interface for Twilio updates. """ + import dataclasses import pydantic @@ -49,6 +50,7 @@ class TwilioProvider(BaseProvider): """Send SMS via Twilio.""" PROVIDER_DISPLAY_NAME = "Twilio" + PROVIDER_CATEGORY = ["Collaboration"] PROVIDER_SCOPES = [ ProviderScope( name="send_sms", @@ -113,11 +115,8 @@ def dispose(self): pass def _notify( - self, - message_body: str = "", - to_phone_number: str = "", - **kwargs: dict - ): + self, message_body: str = "", to_phone_number: str = "", **kwargs: dict + ): """ Notify alert with twilio SMS """ diff --git a/keep/providers/uptimekuma_provider/uptimekuma_provider.py b/keep/providers/uptimekuma_provider/uptimekuma_provider.py index 0179db1b2..f5c8f7728 100644 --- a/keep/providers/uptimekuma_provider/uptimekuma_provider.py +++ b/keep/providers/uptimekuma_provider/uptimekuma_provider.py @@ -51,6 +51,7 @@ class UptimekumaProviderAuthConfig: class UptimekumaProvider(BaseProvider): PROVIDER_DISPLAY_NAME = "UptimeKuma" PROVIDER_TAGS = ["alert"] + PROVIDER_CATEGORY = ["Monitoring"] PROVIDER_SCOPES = [ ProviderScope( diff --git a/keep/providers/victoriametrics_provider/victoriametrics_provider.py b/keep/providers/victoriametrics_provider/victoriametrics_provider.py index f8a140924..15ffe59ff 100644 --- a/keep/providers/victoriametrics_provider/victoriametrics_provider.py +++ b/keep/providers/victoriametrics_provider/victoriametrics_provider.py @@ -89,7 +89,7 @@ class VictoriametricsProvider(BaseProvider): alias="Connect to the client", ), ] - + PROVIDER_CATEGORY = ["Monitoring"] SEVERITIES_MAP = { "critical": AlertSeverity.CRITICAL, "high": AlertSeverity.HIGH, diff --git a/keep/providers/webhook_provider/webhook_provider.py b/keep/providers/webhook_provider/webhook_provider.py index aa6976579..080942354 100644 --- a/keep/providers/webhook_provider/webhook_provider.py +++ b/keep/providers/webhook_provider/webhook_provider.py @@ -87,6 +87,7 @@ class WebhookProvider(BaseProvider): "localhost", "googleapis.com", ] + PROVIDER_CATEGORY = ["Developer Tools"] PROVIDER_SCOPES = [ ProviderScope( diff --git a/keep/providers/zabbix_provider/zabbix_provider.py b/keep/providers/zabbix_provider/zabbix_provider.py index e8d75c7c9..d53c437e4 100644 --- a/keep/providers/zabbix_provider/zabbix_provider.py +++ b/keep/providers/zabbix_provider/zabbix_provider.py @@ -53,6 +53,7 @@ class ZabbixProvider(BaseProvider): Pull/Push alerts from Zabbix into Keep. """ + PROVIDER_CATEGORY = ["Monitoring"] KEEP_ZABBIX_WEBHOOK_INTEGRATION_NAME = "keep" # keep-zabbix KEEP_ZABBIX_WEBHOOK_SCRIPT_FILENAME = ( "zabbix_provider_script.js" # zabbix mediatype script file diff --git a/keep/providers/zendesk_provider/__init__.py b/keep/providers/zendesk_provider/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/keep/providers/zendesk_provider/zendesk_provider.py b/keep/providers/zendesk_provider/zendesk_provider.py new file mode 100644 index 000000000..28e35858e --- /dev/null +++ b/keep/providers/zendesk_provider/zendesk_provider.py @@ -0,0 +1,36 @@ +import dataclasses + +import pydantic + +from keep.contextmanager.contextmanager import ContextManager +from keep.providers.base.base_provider import BaseProvider +from keep.providers.models.provider_config import ProviderConfig + + +@pydantic.dataclasses.dataclass +class ZendeskProviderAuthConfig: + api_key: str = dataclasses.field( + metadata={"required": True, "description": "Zendesk API key", "sensitive": True} + ) + + +class ZendeskProvider(BaseProvider): + PROVIDER_DISPLAY_NAME = "Zendesk" + PROVIDER_CATEGORY = ["Ticketing"] + PROVIDER_COMING_SOON = True + + 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 = ZendeskProviderAuthConfig( + **self.config.authentication + ) + + def dispose(self): + """ + No need to dispose of anything, so just do nothing. + """ + pass diff --git a/keep/providers/zenduty_provider/zenduty_provider.py b/keep/providers/zenduty_provider/zenduty_provider.py index 603e73fbb..23598fa40 100644 --- a/keep/providers/zenduty_provider/zenduty_provider.py +++ b/keep/providers/zenduty_provider/zenduty_provider.py @@ -22,6 +22,7 @@ class ZendutyProvider(BaseProvider): """Create incident in Zenduty.""" PROVIDER_DISPLAY_NAME = "Zenduty" + PROVIDER_CATEGORY = ["Incident Management"] def __init__( self, context_manager: ContextManager, provider_id: str, config: ProviderConfig @@ -40,14 +41,14 @@ def dispose(self): pass def _notify( - self, - title: str = "", - summary: str = "", - service: str = "", - user: str = "", - policy: str = "", - **kwargs: dict - ): + self, + title: str = "", + summary: str = "", + service: str = "", + user: str = "", + policy: str = "", + **kwargs: dict + ): """ Create incident Zenduty using the Zenduty API diff --git a/tests/e2e_tests/test_pushing_prometheus_alerts.py b/tests/e2e_tests/test_pushing_prometheus_alerts.py index d78f768ba..63a14f7cb 100644 --- a/tests/e2e_tests/test_pushing_prometheus_alerts.py +++ b/tests/e2e_tests/test_pushing_prometheus_alerts.py @@ -34,8 +34,10 @@ def test_pulling_prometheus_alerts_to_provider(browser): browser.get_by_placeholder("Filter providers...").click() browser.get_by_placeholder("Filter providers...").fill("prometheus") browser.get_by_placeholder("Filter providers...").press("Enter") - browser.get_by_text("Connect Provider").hover() - prometheus_tile = browser.locator("button:has-text('prometheus'):has-text('alert'):has-text('data')") + browser.get_by_text("Available Providers").hover() + prometheus_tile = browser.locator( + "button:has-text('prometheus'):has-text('alert'):has-text('data')" + ) prometheus_tile.first.hover() prometheus_tile.first.click() browser.get_by_placeholder("Enter provider name").click()