diff --git a/README.md b/README.md index 611ef8f4d4..7bf0bbcea1 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,11 @@
- PRs Welcome - + PRs Welcome Join Slack - GitHub commit activity + + GitHub commit activity diff --git a/keep-ui/app/(keep)/alerts/alert-assignee.tsx b/keep-ui/app/(keep)/alerts/alert-assignee.tsx index c3dc676271..525d00224a 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 34b0b6d3ea..f38add3d4b 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 bf985960e0..41ae7b46c3 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 a26f8ab1dc..51c5ff1969 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 15bc1d9915..83b588c039 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 d1606ffc91..b70e563cdb 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 bd41cbc22a..0f5924559a 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 3fcbff4540..5a27259da9 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 02fe484e0a..6653ea45e1 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 0c803d477e..2249e6d1e5 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 799afbc28e..0da0da3ce8 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 7d3d828e86..7d3f52ef2d 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 && ( + ); + } + return ( diff --git a/keep-ui/app/(keep)/providers/components/providers-categories/index.ts b/keep-ui/app/(keep)/providers/components/providers-categories/index.ts new file mode 100644 index 0000000000..79942cca90 --- /dev/null +++ b/keep-ui/app/(keep)/providers/components/providers-categories/index.ts @@ -0,0 +1 @@ +export { ProvidersCategories } from "./providers-categories"; diff --git a/keep-ui/app/(keep)/providers/components/providers-categories/providers-categories.tsx b/keep-ui/app/(keep)/providers/components/providers-categories/providers-categories.tsx new file mode 100644 index 0000000000..bb657e440a --- /dev/null +++ b/keep-ui/app/(keep)/providers/components/providers-categories/providers-categories.tsx @@ -0,0 +1,53 @@ +import { TProviderCategory } from "@/app/(keep)/providers/providers"; +import { Badge } from "@tremor/react"; +import { useFilterContext } from "../../filter-context"; + +export const ProvidersCategories = () => { + 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 09f696c8e0..669c966f19 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 5d4965038e..604f24bd53 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 1e2866721d..e61f91dbad 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 c56f163b67..aeb6f0d5c2 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 83d2cc7fcf..0c229eef25 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 fd49f8ff9c..b4e4131ad2 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 34949538f2..2e4ef9c951 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 da420f7191..dcc23901b8 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 08f69e1594..69b0c4d178 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 78d2952d25..54cfeb03d6 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/(keep)/topology/api/index.ts b/keep-ui/app/(keep)/topology/api/index.ts index 2d4e3b7f54..5736af7b90 100644 --- a/keep-ui/app/(keep)/topology/api/index.ts +++ b/keep-ui/app/(keep)/topology/api/index.ts @@ -2,19 +2,16 @@ import { fetcher } from "@/utils/fetcher"; import { Session } from "next-auth"; import { TopologyApplication, TopologyService } from "../model/models"; -export function buildTopologyUrl( - apiUrl: string, - { - providerIds, - services, - environment, - }: { - providerIds?: string[]; - services?: string[]; - environment?: string; - } -) { - const baseUrl = `${apiUrl}/topology`; +export function buildTopologyUrl({ + providerIds, + services, + environment, +}: { + providerIds?: string[]; + services?: string[]; + environment?: string; +}) { + const baseUrl = `/topology`; const params = new URLSearchParams(); @@ -57,6 +54,8 @@ export function getTopology( if (!session) { return null; } - const url = buildTopologyUrl(apiUrl, { providerIds, services, environment }); - return fetcher(url, session.accessToken) as Promise; + const url = buildTopologyUrl({ providerIds, services, environment }); + return fetcher(apiUrl + url, session.accessToken) as Promise< + TopologyService[] + >; } diff --git a/keep-ui/app/(keep)/topology/model/useTopology.ts b/keep-ui/app/(keep)/topology/model/useTopology.ts index 24f616f21f..b38ebff03f 100644 --- a/keep-ui/app/(keep)/topology/model/useTopology.ts +++ b/keep-ui/app/(keep)/topology/model/useTopology.ts @@ -7,7 +7,7 @@ import { buildTopologyUrl } from "@/app/(keep)/topology/api"; import { useTopologyPollingContext } from "@/app/(keep)/topology/model/TopologyPollingContext"; import { useApiUrl } from "utils/hooks/useConfig"; -export const useTopologyBaseKey = () => `${useApiUrl()}/topology`; +export const TOPOLOGY_URL = `/topology`; type UseTopologyOptions = { providerIds?: string[]; @@ -37,11 +37,11 @@ export const useTopology = ( const url = !session ? null - : buildTopologyUrl(apiUrl!, { providerIds, services, environment }); + : buildTopologyUrl({ providerIds, services, environment }); const { data, error, mutate } = useSWR( url, - (url: string) => fetcher(url, session!.accessToken), + (url: string) => fetcher(apiUrl! + url, session!.accessToken), { fallbackData, ...options, diff --git a/keep-ui/app/(keep)/topology/model/useTopologyApplications.ts b/keep-ui/app/(keep)/topology/model/useTopologyApplications.ts index eb749d2542..bfc211c653 100644 --- a/keep-ui/app/(keep)/topology/model/useTopologyApplications.ts +++ b/keep-ui/app/(keep)/topology/model/useTopologyApplications.ts @@ -4,14 +4,17 @@ import useSWR, { SWRConfiguration } from "swr"; import { fetcher } from "@/utils/fetcher"; import { useHydratedSession as useSession } from "@/shared/lib/hooks/useHydratedSession"; import { useCallback, useMemo } from "react"; -import { useTopologyBaseKey, useTopology } from "./useTopology"; +import { useTopology } from "./useTopology"; import { useRevalidateMultiple } from "@/utils/state"; +import { TOPOLOGY_URL } from "./useTopology"; type UseTopologyApplicationsOptions = { initialData?: TopologyApplication[]; options?: SWRConfiguration; }; +export const TOPOLOGY_APPLICATIONS_URL = `/topology/applications`; + export function useTopologyApplications( { initialData, options }: UseTopologyApplicationsOptions = { options: { @@ -21,13 +24,11 @@ export function useTopologyApplications( ) { const apiUrl = useApiUrl(); const { data: session } = useSession(); - const topologyBaseKey = useTopologyBaseKey(); const revalidateMultiple = useRevalidateMultiple(); const { topologyData, mutate: mutateTopology } = useTopology(); - const topologyApplicationsKey = `${apiUrl}/topology/applications`; const { data, error, isLoading, mutate } = useSWR( - !session ? null : topologyApplicationsKey, - (url: string) => fetcher(url, session!.accessToken), + !session ? null : TOPOLOGY_APPLICATIONS_URL, + (url: string) => fetcher(apiUrl + url, session!.accessToken), { fallbackData: initialData, ...options, @@ -48,7 +49,7 @@ export function useTopologyApplications( }); if (response.ok) { console.log("mutating on success"); - revalidateMultiple([topologyBaseKey, topologyApplicationsKey]); + revalidateMultiple([TOPOLOGY_URL, TOPOLOGY_APPLICATIONS_URL]); } else { // Rollback optimistic update on error throw new Error("Failed to add application", { @@ -58,7 +59,7 @@ export function useTopologyApplications( const json = await response.json(); return json as TopologyApplication; }, - [revalidateMultiple, session?.accessToken, topologyApplicationsKey] + [apiUrl, revalidateMultiple, session?.accessToken] ); const updateApplication = useCallback( @@ -98,7 +99,7 @@ export function useTopologyApplications( } ); if (response.ok) { - revalidateMultiple([topologyBaseKey, topologyApplicationsKey]); + revalidateMultiple([TOPOLOGY_URL, TOPOLOGY_APPLICATIONS_URL]); } else { // Rollback optimistic update on error mutate(applications, false); @@ -110,12 +111,12 @@ export function useTopologyApplications( return response; }, [ + apiUrl, applications, mutate, mutateTopology, revalidateMultiple, session?.accessToken, - topologyApplicationsKey, topologyData, ] ); @@ -152,7 +153,7 @@ export function useTopologyApplications( } ); if (response.ok) { - revalidateMultiple([topologyBaseKey, topologyApplicationsKey]); + revalidateMultiple([TOPOLOGY_URL, TOPOLOGY_APPLICATIONS_URL]); } else { // Rollback optimistic update on error mutate(applications, false); @@ -164,12 +165,12 @@ export function useTopologyApplications( return response; }, [ + apiUrl, applications, mutate, mutateTopology, revalidateMultiple, session?.accessToken, - topologyApplicationsKey, topologyData, ] ); diff --git a/keep-ui/app/(signin)/layout.tsx b/keep-ui/app/(signin)/layout.tsx index 5ff842ef07..c535b8c488 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/app/api/aws-marketplace/route.ts b/keep-ui/app/api/aws-marketplace/route.ts new file mode 100644 index 0000000000..f8c4bb07ca --- /dev/null +++ b/keep-ui/app/api/aws-marketplace/route.ts @@ -0,0 +1,21 @@ +import { NextRequest } from "next/server"; +import { redirect } from "next/navigation"; + +export async function POST(request: NextRequest) { + try { + // In App Router, we need to parse the request body manually + const body = await request.json(); + + const token = body["x-amzn-marketplace-token"]; + const offerType = body["x-amzn-marketplace-offer-type"]; + + // Base64 encode the token + const base64EncodedToken = encodeURIComponent(btoa(token)); + + // In App Router, we use the redirect function for redirects + return redirect(`/signin?amt=${base64EncodedToken}`); + } catch (error) { + console.error("Error processing request:", error); + return new Response("Bad Request", { status: 400 }); + } +} diff --git a/keep-ui/app/api/copilotkit/route.ts b/keep-ui/app/api/copilotkit/route.ts new file mode 100644 index 0000000000..e8cde87f1e --- /dev/null +++ b/keep-ui/app/api/copilotkit/route.ts @@ -0,0 +1,41 @@ +import { + CopilotRuntime, + OpenAIAdapter, + copilotRuntimeNextJSAppRouterEndpoint, +} from "@copilotkit/runtime"; +import OpenAI, { OpenAIError } from "openai"; +import { NextRequest } from "next/server"; + +function initializeCopilotRuntime() { + try { + const openai = new OpenAI({ + organization: process.env.OPEN_AI_ORGANIZATION_ID, + apiKey: process.env.OPEN_AI_API_KEY, + }); + const serviceAdapter = new OpenAIAdapter({ openai }); + const runtime = new CopilotRuntime(); + return { runtime, serviceAdapter }; + } catch (error) { + if (error instanceof OpenAIError) { + console.log("Error connecting to OpenAI", error); + } else { + console.error("Error initializing Copilot Runtime", error); + } + return null; + } +} + +const runtimeOptions = initializeCopilotRuntime(); + +export const POST = async (req: NextRequest) => { + if (!runtimeOptions) { + return new Response("Error initializing Copilot Runtime", { status: 500 }); + } + const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({ + runtime: runtimeOptions.runtime, + serviceAdapter: runtimeOptions.serviceAdapter, + endpoint: "/api/copilotkit", + }); + + return handleRequest(req); +}; diff --git a/keep-ui/components/navbar/UserAvatar.tsx b/keep-ui/components/navbar/UserAvatar.tsx index d903003636..c2ae3c9963 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 0000000000..f765314cd8 --- /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 48368b77c7..d87bb235fb 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 0000000000..53d9bf43d8 --- /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 0000000000..b419f1b440 --- /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 ddf50f976c..0458f60078 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 f71c702a50..7f34c5813f 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/next-env.d.ts b/keep-ui/next-env.d.ts index 725dd6f245..40c3d68096 100644 --- a/keep-ui/next-env.d.ts +++ b/keep-ui/next-env.d.ts @@ -1,6 +1,5 @@ /// /// -/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/keep-ui/package-lock.json b/keep-ui/package-lock.json index 0d13111158..c6e0749ac0 100644 --- a/keep-ui/package-lock.json +++ b/keep-ui/package-lock.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", @@ -55,7 +56,7 @@ "moment": "^2.29.4", "next": "^14.2.13", "next-auth": "^5.0.0-beta.25", - "openai": "^4.72.0", + "openai": "^4.73.0", "postcss": "^8.4.31", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", @@ -6322,6 +6323,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", @@ -16126,9 +16160,9 @@ } }, "node_modules/openai": { - "version": "4.72.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.72.0.tgz", - "integrity": "sha512-hFqG9BWCs7L7ifrhJXw7mJXmUBr7d9N6If3J9563o0jfwVA4wFANFDDaOIWFdgDdwgCXg5emf0Q+LoLCGszQYA==", + "version": "4.73.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.73.0.tgz", + "integrity": "sha512-NZstV77w3CEol9KQTRBRQ15+Sw6nxVTicAULSjYO4wn9E5gw72Mtp3fAVaBFXyyVPws4241YmFG6ya4L8v03tA==", "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", diff --git a/keep-ui/package.json b/keep-ui/package.json index f485667278..4678a06dc4 100644 --- a/keep-ui/package.json +++ b/keep-ui/package.json @@ -26,6 +26,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", @@ -56,7 +57,7 @@ "moment": "^2.29.4", "next": "^14.2.13", "next-auth": "^5.0.0-beta.25", - "openai": "^4.72.0", + "openai": "^4.73.0", "postcss": "^8.4.31", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", diff --git a/keep-ui/pages/_error.jsx b/keep-ui/pages/_error.jsx deleted file mode 100644 index 46a61d690c..0000000000 --- a/keep-ui/pages/_error.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import * as Sentry from "@sentry/nextjs"; -import Error from "next/error"; - -const CustomErrorComponent = (props) => { - return ; -}; - -CustomErrorComponent.getInitialProps = async (contextData) => { - // In case this is running in a serverless function, await this in order to give Sentry - // time to send the error before the lambda exits - await Sentry.captureUnderscoreErrorException(contextData); - - // This will contain the status code of the response - return Error.getInitialProps(contextData); -}; - -export default CustomErrorComponent; diff --git a/keep-ui/pages/api/aws-marketplace.tsx b/keep-ui/pages/api/aws-marketplace.tsx deleted file mode 100644 index 8d5b8d2035..0000000000 --- a/keep-ui/pages/api/aws-marketplace.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - if (req.method === "POST") { - const { - "x-amzn-marketplace-token": token, - "x-amzn-marketplace-offer-type": offerType, - } = req.body; - - const base64EncodedToken = encodeURIComponent(btoa(token)); - - // Redirect to the sign-in page or wherever you want - // amt is amazon-marketplace-token - res.writeHead(302, { Location: `/signin?amt=${base64EncodedToken}` }); - res.end(); - } else { - // Handle any non-POST requests - res.status(405).send("Method Not Allowed"); - } -} diff --git a/keep-ui/pages/api/copilotkit.ts b/keep-ui/pages/api/copilotkit.ts deleted file mode 100644 index 2af1161ab1..0000000000 --- a/keep-ui/pages/api/copilotkit.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NextApiRequest, NextApiResponse } from "next"; -import { - CopilotRuntime, - OpenAIAdapter, - copilotRuntimeNextJSPagesRouterEndpoint, -} from "@copilotkit/runtime"; -import OpenAI from "openai"; - -const openai = new OpenAI({ - organization: process.env.OPEN_AI_ORGANIZATION_ID, - apiKey: process.env.OPEN_AI_API_KEY, -}); - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - const serviceAdapter = new OpenAIAdapter({ openai }); - const runtime = new CopilotRuntime(); - - const handleRequest = copilotRuntimeNextJSPagesRouterEndpoint({ - endpoint: "/api/copilotkit", - runtime, - serviceAdapter, - }); - - return await handleRequest(req, res); -} diff --git a/keep-ui/public/icons/salesforce-icon.png b/keep-ui/public/icons/salesforce-icon.png new file mode 100644 index 0000000000..25efdfea8f 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 0000000000..dd0ce0082b Binary files /dev/null and b/keep-ui/public/icons/zendesk-icon.png differ diff --git a/keep-ui/shared/lib/server/getConfig.ts b/keep-ui/shared/lib/server/getConfig.ts index 7a823ed15f..b10cbbe702 100644 --- a/keep-ui/shared/lib/server/getConfig.ts +++ b/keep-ui/shared/lib/server/getConfig.ts @@ -54,5 +54,6 @@ export function getConfig() { POSTHOG_HOST: process.env.POSTHOG_HOST, SENTRY_DISABLED: process.env.SENTRY_DISABLED, READ_ONLY: process.env.KEEP_READ_ONLY === "true", + OPEN_AI_API_KEY_SET: !!process.env.OPEN_AI_API_KEY, }; } diff --git a/keep-ui/shared/lib/status-utils.ts b/keep-ui/shared/lib/status-utils.ts new file mode 100644 index 0000000000..7eabd2f8f1 --- /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/EmptyState/EmptyStateCard.tsx b/keep-ui/shared/ui/EmptyState/EmptyStateCard.tsx new file mode 100644 index 0000000000..c683e3e0ec --- /dev/null +++ b/keep-ui/shared/ui/EmptyState/EmptyStateCard.tsx @@ -0,0 +1,36 @@ +import { Card } from "@tremor/react"; +import { CircleStackIcon } from "@heroicons/react/24/outline"; +import clsx from "clsx"; + +export function EmptyStateCard({ + title, + icon, + description, + className, + children, +}: { + icon?: React.ElementType; + title: string; + description: string; + className?: string; + children?: React.ReactNode; +}) { + const Icon = icon || CircleStackIcon; + return ( + +
+ +

+ {title} +

+

+ {description} +

+ {children} +
+
+ ); +} diff --git a/keep-ui/shared/ui/EmptyState/index.ts b/keep-ui/shared/ui/EmptyState/index.ts new file mode 100644 index 0000000000..a31d06c86e --- /dev/null +++ b/keep-ui/shared/ui/EmptyState/index.ts @@ -0,0 +1 @@ +export { EmptyStateCard } from "./EmptyStateCard"; diff --git a/keep-ui/shared/ui/FieldHeader.tsx b/keep-ui/shared/ui/FieldHeader.tsx index 5c99f4c235..c40813331f 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 0000000000..7da4c271cf --- /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 0000000000..cf5cc016b7 --- /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 4cfab58102..fc196b3557 100644 --- a/keep-ui/shared/ui/index.ts +++ b/keep-ui/shared/ui/index.ts @@ -2,3 +2,7 @@ export { TablePagination } from "./TablePagination"; 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 da4514760e..9553ecaf26 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-ui/types/internal-config.ts b/keep-ui/types/internal-config.ts index 550828b27c..c0d13710b2 100644 --- a/keep-ui/types/internal-config.ts +++ b/keep-ui/types/internal-config.ts @@ -18,4 +18,5 @@ export interface InternalConfig { SENTRY_DISABLED: string; // READ ONLY READ_ONLY: boolean; + OPEN_AI_API_KEY_SET: boolean; } diff --git a/keep-ui/utils/hooks/usePresets.ts b/keep-ui/utils/hooks/usePresets.ts index 6cc10873d5..a3c72ec5ee 100644 --- a/keep-ui/utils/hooks/usePresets.ts +++ b/keep-ui/utils/hooks/usePresets.ts @@ -9,6 +9,7 @@ import { useConfig } from "./useConfig"; import useSWRSubscription from "swr/subscription"; import { useWebsocket } from "./usePusher"; import { useSearchParams } from "next/navigation"; +import { useRevalidateMultiple } from "../state"; export const usePresets = (type?: string, useFilters?: boolean) => { const { data: session } = useSession(); @@ -31,6 +32,7 @@ export const usePresets = (type?: string, useFilters?: boolean) => { const presetsOrderRef = useRef(presetsOrderFromLS); const staticPresetsOrderRef = useRef(staticPresetsOrderFromLS); const { bind, unbind } = useWebsocket(); + const revalidateMultiple = useRevalidateMultiple(); useEffect(() => { presetsOrderRef.current = presetsOrderFromLS; @@ -88,6 +90,8 @@ export const usePresets = (type?: string, useFilters?: boolean) => { (_, { next }) => { const newPresets = (newPresets: Preset[]) => { updateLocalPresets(newPresets); + // update the presets aggregated endpoint for the sidebar + revalidateMultiple(["/preset"]); next(null, { presets: newPresets, isAsyncLoading: false, @@ -110,11 +114,11 @@ export const usePresets = (type?: string, useFilters?: boolean) => { return useSWR( () => session - ? `${apiUrl}/preset${ + ? `/preset${ useFilters && filters && isDashBoard ? `?${filters}` : "" }` : null, - (url) => fetcher(url, session?.accessToken), + (url) => fetcher(apiUrl + url, session?.accessToken), { ...options, onSuccess: (data) => { diff --git a/keep-ui/utils/state.ts b/keep-ui/utils/state.ts index 517ad49f9d..ded186d6a9 100644 --- a/keep-ui/utils/state.ts +++ b/keep-ui/utils/state.ts @@ -1,17 +1,9 @@ import { useSWRConfig } from "swr"; -type MutateArgs = [string, (data: any) => any]; - -export const mutateLocalMultiple = (args: MutateArgs[]) => { - const { cache } = useSWRConfig(); - args.forEach(([key, mutateFunction]) => { - const currentData = cache.get(key as string); - cache.set(key as string, mutateFunction(currentData)); - }); -}; - export const useRevalidateMultiple = () => { const { mutate } = useSWRConfig(); return (keys: string[]) => - mutate((key) => typeof key === "string" && keys.includes(key)); + mutate( + (key) => typeof key === "string" && keys.some((k) => key.startsWith(k)) + ); }; diff --git a/keep/api/core/db.py b/keep/api/core/db.py index f374117eed..9be0d25f70 100644 --- a/keep/api/core/db.py +++ b/keep/api/core/db.py @@ -40,6 +40,7 @@ from sqlalchemy.sql import exists, expression from sqlmodel import Session, SQLModel, col, or_, select, text +from keep.api.consts import STATIC_PRESETS from keep.api.core.db_utils import create_db_engine, get_json_extract_field # This import is required to create the tables @@ -2615,7 +2616,7 @@ def get_presets( return presets -def get_preset_by_name(tenant_id: str, preset_name: str) -> Preset: +def get_db_preset_by_name(tenant_id: str, preset_name: str) -> Preset | None: with Session(engine) as session: preset = session.exec( select(Preset) @@ -2624,8 +2625,7 @@ def get_preset_by_name(tenant_id: str, preset_name: str) -> Preset: ).first() return preset - -def get_all_presets(tenant_id: str) -> List[Preset]: +def get_db_presets(tenant_id: str) -> List[Preset]: with Session(engine) as session: presets = ( session.exec(select(Preset).where(Preset.tenant_id == tenant_id)) @@ -2634,6 +2634,10 @@ def get_all_presets(tenant_id: str) -> List[Preset]: ) return presets +def get_all_presets_dtos(tenant_id: str) -> List[PresetDto]: + presets = get_db_presets(tenant_id) + static_presets_dtos = list(STATIC_PRESETS.values()) + return [PresetDto(**preset.to_dict()) for preset in presets] + static_presets_dtos def get_dashboards(tenant_id: str, email=None) -> List[Dict[str, Any]]: with Session(engine) as session: diff --git a/keep/api/core/demo_mode.py b/keep/api/core/demo_mode.py index 26ec63addb..4ffc061660 100644 --- a/keep/api/core/demo_mode.py +++ b/keep/api/core/demo_mode.py @@ -29,7 +29,7 @@ { "sqlQuery": {"sql": "((name like :name_1))", "params": {"name_1": "%mq%"}}, "groupDescription": "This rule groups all alerts related to MQ.", - "ruleName": "Message Queue Buckle Up", + "ruleName": "Message queue is getting filled up", "celQuery": '(name.contains("mq"))', "timeframeInSeconds": 86400, "timeUnit": "hours", @@ -243,6 +243,14 @@ def get_or_create_topology(keep_api_key, keep_api_url): if service["name"] == existing_service["display_name"]: service["id"] = existing_service["id"] + # Check if any service does not have an id + for service in application_to_create["services"]: + if "id" not in service: + logger.error( + f"Service {service['name']} does not have an id. Application creation failed." + ) + return True + response = requests.post( f"{keep_api_url}/topology/applications", headers={"x-api-key": keep_api_key}, @@ -415,21 +423,22 @@ def simulate_alerts( time.sleep(sleep_interval) -def launch_demo_mode_thread(keep_api_url=None) -> threading.Thread | None: +def launch_demo_mode_thread(keep_api_url=None, keep_api_key=None) -> threading.Thread | None: if not KEEP_LIVE_DEMO_MODE: logger.info("Not launching the demo mode.") return logger.info("Launching demo mode.") - with get_session_sync() as session: - keep_api_key = get_or_create_api_key( - session=session, - tenant_id=SINGLE_TENANT_UUID, - created_by="system", - unique_api_key_id="simulate_alerts", - system_description="Simulate Alerts API key", - ) + if keep_api_key is None: + with get_session_sync() as session: + keep_api_key = get_or_create_api_key( + session=session, + tenant_id=SINGLE_TENANT_UUID, + created_by="system", + unique_api_key_id="simulate_alerts", + system_description="Simulate Alerts API key", + ) sleep_interval = 5 diff --git a/keep/api/models/provider.py b/keep/api/models/provider.py index 1b3faebd17..7fbc4e3f31 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/api/routes/preset.py b/keep/api/routes/preset.py index 1f3f9b22a1..bd331806ce 100644 --- a/keep/api/routes/preset.py +++ b/keep/api/routes/preset.py @@ -15,7 +15,7 @@ from sqlmodel import Session, select from keep.api.consts import PROVIDER_PULL_INTERVAL_DAYS, STATIC_PRESETS -from keep.api.core.db import get_preset_by_name as get_preset_by_name_db +from keep.api.core.db import get_db_preset_by_name from keep.api.core.db import get_presets as get_presets_db from keep.api.core.db import ( get_session, @@ -448,7 +448,7 @@ def get_preset_alerts( if preset_name in STATIC_PRESETS: preset = STATIC_PRESETS[preset_name] else: - preset = get_preset_by_name_db(tenant_id, preset_name) + preset = get_db_preset_by_name(tenant_id, preset_name) # if preset does not exist if not preset: raise HTTPException(404, "Preset not found") diff --git a/keep/api/tasks/process_event_task.py b/keep/api/tasks/process_event_task.py index a4bec5e8c2..3809ea8721 100644 --- a/keep/api/tasks/process_event_task.py +++ b/keep/api/tasks/process_event_task.py @@ -20,7 +20,7 @@ from keep.api.core.db import ( bulk_upsert_alert_fields, get_alerts_by_fingerprint, - get_all_presets, + get_all_presets_dtos, get_enrichment_with_session, get_session_sync, ) @@ -28,7 +28,6 @@ from keep.api.core.elastic import ElasticClient from keep.api.models.alert import AlertDto, AlertStatus, IncidentDto from keep.api.models.db.alert import Alert, AlertActionType, AlertAudit, AlertRaw -from keep.api.models.db.preset import PresetDto from keep.api.utils.enrichment_helpers import ( calculated_start_firing_time, convert_db_alerts_to_dto_alerts, @@ -443,12 +442,11 @@ def __handle_formatted_events( return try: - presets = get_all_presets(tenant_id) + presets = get_all_presets_dtos(tenant_id) rules_engine = RulesEngine(tenant_id=tenant_id) presets_do_update = [] - for preset in presets: + for preset_dto in presets: # filter the alerts based on the search query - preset_dto = PresetDto(**preset.to_dict()) filtered_alerts = rules_engine.filter_alerts( enriched_formatted_events, preset_dto.cel_query ) @@ -458,7 +456,7 @@ def __handle_formatted_events( presets_do_update.append(preset_dto) preset_dto.alerts_count = len(filtered_alerts) # update noisy - if preset.is_noisy: + if preset_dto.is_noisy: firing_filtered_alerts = list( filter( lambda alert: alert.status == AlertStatus.FIRING.value, diff --git a/keep/providers/aks_provider/aks_provider.py b/keep/providers/aks_provider/aks_provider.py index 937bbf5913..c0199977b8 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 2abd4bae22..15fd3b3008 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 e61598ad0f..8e29289fb1 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 e60e42494a..632dfddb71 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 43793ae3cb..5669af33ab 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 493c29001e..cea8ca82b8 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 45e186963d..8aabe4b8a5 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 7d2f3e4feb..4bb5a343d8 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 c9aa19be99..14dd763bd4 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 522bc9218d..42d992cea8 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 4b35cefeec..2b9d2c9f8f 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 be200f8caa..7552edeedc 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 1ed543ddeb..59aa4faa08 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/alerts_mock.py b/keep/providers/datadog_provider/alerts_mock.py index 0ff032a3b9..f4fcff3ce2 100644 --- a/keep/providers/datadog_provider/alerts_mock.py +++ b/keep/providers/datadog_provider/alerts_mock.py @@ -35,4 +35,22 @@ "priority": ["P1", "P3", "P4"], }, }, + "mq_consumer_struggling": { + "payload": { + "title": "mq consumer is struggling", + "type": "metric alert", + "query": "avg(last_1h):min:mq_processing{*} by {host} < 10", + "message": "MQ Consumer is processing less than 10 messages per second on {{host.name}}.", + "tags": "environment:production,team:database", + "priority": 4, + "monitor_id": "1234567891", + }, + "parameters": { + "tags": [ + "environment:production,team:analytics,monitor,service:api", + "environment:staging,team:database,monitor,service:api", + ], + "priority": ["P1", "P3", "P4"], + }, + }, } diff --git a/keep/providers/datadog_provider/datadog_provider.py b/keep/providers/datadog_provider/datadog_provider.py index e19881f5d3..5710433ae3 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 a9bd7dfb39..c35cf503a0 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 67e702a363..5f61c22e67 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 f6b4dae24c..a194f00d88 100644 --- a/keep/providers/elastic_provider/elastic_provider.py +++ b/keep/providers/elastic_provider/elastic_provider.py @@ -1,6 +1,7 @@ """ Elasticsearch provider. """ + import dataclasses import json @@ -46,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 a782809447..8230ccff6c 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 c4d524a8dd..8a91721261 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 90563a15e1..9d04122f71 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 2aa34585c4..64431c1102 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 2b44742e23..92d5016b7f 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 ba27d46243..1b749dc08d 100644 --- a/keep/providers/google_chat_provider/google_chat_provider.py +++ b/keep/providers/google_chat_provider/google_chat_provider.py @@ -1,6 +1,9 @@ +import dataclasses +import http import os +import time + import pydantic -import dataclasses import requests from keep.contextmanager.contextmanager import ContextManager @@ -29,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) @@ -65,15 +69,35 @@ def _notify(self, message="", **kwargs: dict): if not message: raise ProviderException("Message is required") + def __send_message(url, body, headers, retries=3): + for attempt in range(retries): + try: + resp = requests.post(url, json=body, headers=headers) + if resp.status_code == http.HTTPStatus.OK: + return resp + + 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}") + + if attempt < retries - 1: + time.sleep(1) + + raise requests.exceptions.RequestException( + f"Failed to notify message after {retries} attempts" + ) + payload = { "text": message, } - requestHeaders = {"Content-Type": "application/json; charset=UTF-8"} - - response = requests.post(webhook_url, json=payload, headers=requestHeaders) + request_headers = {"Content-Type": "application/json; charset=UTF-8"} - if not response.ok: + 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}" ) diff --git a/keep/providers/grafana_incident_provider/grafana_incident_provider.py b/keep/providers/grafana_incident_provider/grafana_incident_provider.py index c8474e9b0a..8f2522d6b7 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 d264f77cbc..91e0a2fd2a 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 b3b9cf1f6b..a343eaa3ed 100644 --- a/keep/providers/grafana_provider/grafana_provider.py +++ b/keep/providers/grafana_provider/grafana_provider.py @@ -49,6 +49,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 a59156d65f..d35037c533 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 748e67c63d..8cb839fa9e 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 77cfe634ec..acd7ecb122 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 6abdc00dba..06fa5258d3 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 b792165949..e0cf9048de 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 f6f48379e9..907b7069b6 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 d79a1c2933..993b9e4ab1 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 98a7273e89..6bd7238cb8 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 2dd85709bf..fb7e5417e7 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 804743579f..a8d2801bbc 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 c04fce6647..b3512c652e 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 8e17d1d8a2..8a69d2adea 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 36b8a57170..84c9879b77 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 4a2d6b3129..aa2dd9298d 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 7612e1f975..c95302c075 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 50bd7d9398..c8c5b61b2a 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 5ace58a75d..1c2da6199f 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 114ef40e8b..883c20ec69 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 49ecaa52ba..82b11150de 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 992f750cb5..fbcce89fbf 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 ed107fbedc..261e618e51 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 ca65b2a081..013801b635 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 acfd90f6f1..ad03bfb40a 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 231673dd5a..36294641ee 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 598cf091b5..2118a2a9c3 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 dd900fda58..64cd971085 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 ef69807711..70d5caee88 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 87b27dde30..62861df925 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 1191f80db8..0290a554cd 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/alerts_mock.py b/keep/providers/prometheus_provider/alerts_mock.py index d29197074a..fa5f7e9223 100644 --- a/keep/providers/prometheus_provider/alerts_mock.py +++ b/keep/providers/prometheus_provider/alerts_mock.py @@ -15,7 +15,7 @@ "labels.instance": ["instance1", "instance2", "instance3"], }, }, - "mq_third_full": { + "mq_third_full (Message queue is over 33%)": { "payload": { "summary": "Message queue is over 33% capacity", "labels": { @@ -29,6 +29,20 @@ "labels.mq_manager": ["mq_manager1", "mq_manager2", "mq_manager3"], }, }, + "mq_full (Message queue is full)": { + "payload": { + "summary": "Message queue is over 90% capacity", + "labels": { + "severity": "critical", + "customer_id": "acme" + }, + }, + "parameters": { + "labels.queue": ["queue4"], + "labels.service": ["calendar-producer-java-otel-api-dd", "kafka", "queue"], + "labels.mq_manager": ["mq_manager4"], + }, + }, "disk_space_low": { "payload": { "summary": "Disk space is below 20%", diff --git a/keep/providers/prometheus_provider/prometheus_provider.py b/keep/providers/prometheus_provider/prometheus_provider.py index 19d57f9df8..5f4d96d409 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 96e13287dc..f4000efb69 100644 --- a/keep/providers/providers_factory.py +++ b/keep/providers/providers_factory.py @@ -22,7 +22,11 @@ from keep.api.models.alert import DeduplicationRuleDto from keep.api.models.provider import Provider from keep.contextmanager.contextmanager import ContextManager -from keep.providers.base.base_provider import BaseProvider, BaseTopologyProvider, BaseIncidentProvider +from keep.providers.base.base_provider import ( + BaseIncidentProvider, + BaseProvider, + BaseTopologyProvider, +) from keep.providers.models.provider_config import ProviderConfig from keep.providers.models.provider_method import ProviderMethodDTO, ProviderMethodParam from keep.secretmanager.secretmanagerfactory import SecretManagerFactory @@ -38,7 +42,9 @@ class ProvidersFactory: _loaded_providers_cache = None @staticmethod - def get_provider_class(provider_type: str) -> BaseProvider | BaseTopologyProvider | BaseIncidentProvider: + def get_provider_class( + provider_type: str, + ) -> BaseProvider | BaseTopologyProvider | BaseIncidentProvider: provider_type_split = provider_type.split( "." ) # e.g. "cloudwatch.logs" or "cloudwatch.metrics" @@ -267,7 +273,8 @@ def get_all_providers() -> list[Provider]: and provider_class.__dict__.get("setup_webhook") is not None ) or ( issubclass(provider_class, BaseIncidentProvider) - and provider_class.__dict__.get("setup_incident_webhook") is not None + and provider_class.__dict__.get("setup_incident_webhook") + is not None ) webhook_required = provider_class.WEBHOOK_INSTALLATION_REQUIRED supports_webhook = ( @@ -376,6 +383,8 @@ def get_all_providers() -> 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 dd4bda525c..b67e9d8bb5 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 2b19b17641..b72bf66c2d 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 27150f3e49..7364e84485 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 2a7734e39a..9509f61524 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 41b6d9007e..c0fa39c547 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 0000000000..e69de29bb2 diff --git a/keep/providers/salesforce_provider/salesforce_provider.py b/keep/providers/salesforce_provider/salesforce_provider.py new file mode 100644 index 0000000000..f1d252de3d --- /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 db14f0b129..3ed4e90b66 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 5a6d615d4d..42c2693f0b 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 307dea4966..d08c09f976 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 0b490bdd53..3987d70c30 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 9897a32486..3bafdde1b7 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 c474155e5f..d2fe50fbb6 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 e1bef052e3..84c44a8504 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 f8f187c09b..3588da81c1 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 edd10a8df9..fd5fbd1692 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 bbdfb79804..440f23b435 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 412b40d407..3a708f2e49 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 e48f9d0ad3..7b5f34eaf3 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 ec526648d5..9d3977ae75 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 9ffb79bc26..35ee798306 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 1749677e2f..d7898e9a59 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 c4e0dd04b0..b69a1aeab1 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 6f80e90c9d..1d97bd8d1a 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 3dea3ed9d1..17c492730f 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 0179db1b27..f5c8f7728a 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 f8a1409244..15ffe59ffe 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 aa69765795..0809423548 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 e8d75c7c93..d53c437e42 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 0000000000..e69de29bb2 diff --git a/keep/providers/zendesk_provider/zendesk_provider.py b/keep/providers/zendesk_provider/zendesk_provider.py new file mode 100644 index 0000000000..28e35858e8 --- /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 603e73fbb1..23598fa400 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/keep/server_jobs_bg.py b/keep/server_jobs_bg.py index b1cad63907..bce6ca9c09 100644 --- a/keep/server_jobs_bg.py +++ b/keep/server_jobs_bg.py @@ -15,6 +15,7 @@ def main(): # We intentionally don't use KEEP_API_URL here to avoid going through the internet. # Script should be launched in the same environment as the server. keep_api_url = "http://localhost:" + str(os.environ.get("PORT", 8080)) + keep_api_key = os.environ.get("KEEP_LIVE_DEMO_MODE_API_KEY") while True: try: @@ -27,7 +28,7 @@ def main(): time.sleep(5) threads = [] - threads.append(launch_demo_mode_thread(keep_api_url)) + threads.append(launch_demo_mode_thread(keep_api_url, keep_api_key)) threads.append(launch_uptime_reporting_thread()) logger.info("Background server jobs threads launched, joining them.") diff --git a/tests/e2e_tests/test_pushing_prometheus_alerts.py b/tests/e2e_tests/test_pushing_prometheus_alerts.py index d78f768ba1..63a14f7cb4 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()