diff --git a/docker-compose.yml b/docker-compose.yml index 14b6001ad..8358c4edc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,8 @@ services: image: us-central1-docker.pkg.dev/keephq/keep/keep-api environment: - AUTH_TYPE=NO_AUTH + - PROMETHEUS_MULTIPROC_DIR=/tmp/prometheus + - KEEP_METRICS=true volumes: - ./state:/state diff --git a/keep-ui/features/filter/facets-panel.tsx b/keep-ui/features/filter/facets-panel.tsx index 2b26120fb..e7448d40b 100644 --- a/keep-ui/features/filter/facets-panel.tsx +++ b/keep-ui/features/filter/facets-panel.tsx @@ -1,10 +1,16 @@ import React, { useEffect, useMemo, useState } from "react"; import { Facet } from "./facet"; -import { CreateFacetDto, FacetDto, FacetOptionDto, FacetOptionsQueries } from "./models"; +import { + CreateFacetDto, + FacetDto, + FacetOptionDto, + FacetOptionsQueries, +} from "./models"; import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { useLocalStorage } from "@/utils/hooks/useLocalStorage"; import { AddFacetModal } from "./add-facet-modal"; -import 'react-loading-skeleton/dist/skeleton.css'; +import "react-loading-skeleton/dist/skeleton.css"; +import clsx from "clsx"; /** * It's facets state. Key is the facet id, and value is Set of unselected options. @@ -12,34 +18,40 @@ import 'react-loading-skeleton/dist/skeleton.css'; */ type FacetState = { [facetId: string]: Set; -} +}; -function buildCel(facets: FacetDto[], facetOptions: { [key: string]: FacetOptionDto[] }, facetsState: FacetState): string { +function buildCel( + facets: FacetDto[], + facetOptions: { [key: string]: FacetOptionDto[] }, + facetsState: FacetState +): string { const cel = Object.values(facets) - .filter((facet) => facet.id in facetsState) - .map((facet) => { - const notSelectedOptions = Object.values(facetOptions[facet.id]) - .filter((facetOption) => facetsState[facet.id]?.has(facetOption.display_name)) - .map((option) => { - if (typeof option.value === 'string') { - return `'${option.value}'`; - } else if (option.value == null) { - return 'null'; - } - - return option.value; - }); - - if (!notSelectedOptions.length) { - return; + .filter((facet) => facet.id in facetsState) + .map((facet) => { + const notSelectedOptions = Object.values(facetOptions[facet.id]) + .filter((facetOption) => + facetsState[facet.id]?.has(facetOption.display_name) + ) + .map((option) => { + if (typeof option.value === "string") { + return `'${option.value}'`; + } else if (option.value == null) { + return "null"; } - return `!(${facet.property_path} in [${notSelectedOptions.join(", ")}])`; - }) - .filter((query) => query) - .map((facetCel) => `${facetCel}`) - .map((query) => query) - .join(" && "); + return option.value; + }); + + if (!notSelectedOptions.length) { + return; + } + + return `!(${facet.property_path} in [${notSelectedOptions.join(", ")}])`; + }) + .filter((query) => query) + .map((facetCel) => `${facetCel}`) + .map((query) => query) + .join(" && "); return cel; } @@ -52,13 +64,19 @@ export interface FacetsPanelProps { areFacetOptionsLoading?: boolean; /** Token to clear filters related to facets */ clearFiltersToken?: string | null; - /** + /** * Object with facets that should be unchecked by default. * Key is the facet name, value is the list of option values to uncheck. **/ uncheckedByDefaultOptionValues?: { [key: string]: string[] }; - renderFacetOptionLabel?: (facetName: string, optionDisplayName: string) => JSX.Element | string | undefined; - renderFacetOptionIcon?: (facetName: string, optionDisplayName: string) => JSX.Element | undefined; + renderFacetOptionLabel?: ( + facetName: string, + optionDisplayName: string + ) => JSX.Element | string | undefined; + renderFacetOptionIcon?: ( + facetName: string, + optionDisplayName: string + ) => JSX.Element | undefined; onCelChange: (cel: string) => void; onAddFacet: (createFacet: CreateFacetDto) => void; onDeleteFacet: (facetId: string) => void; @@ -93,12 +111,18 @@ export const FacetsPanel: React.FC = ({ const [celState, setCelState] = useState(""); function getFacetState(facetId: string): Set { - if (!defaultStateHandledForFacetIds.has(facetId) && uncheckedByDefaultOptionValues && Object.keys(uncheckedByDefaultOptionValues).length) { + if ( + !defaultStateHandledForFacetIds.has(facetId) && + uncheckedByDefaultOptionValues && + Object.keys(uncheckedByDefaultOptionValues).length + ) { const facetState = new Set(...(facetsState[facetId] || [])); const facet = facets.find((f) => f.id === facetId); if (facet) { - uncheckedByDefaultOptionValues[facet?.name]?.forEach((optionValue) => facetState.add(optionValue)); + uncheckedByDefaultOptionValues[facet?.name]?.forEach((optionValue) => + facetState.add(optionValue) + ); defaultStateHandledForFacetIds.add(facetId); } @@ -110,7 +134,7 @@ export const FacetsPanel: React.FC = ({ const isOptionSelected = (facet_id: string, option_id: string) => { return !facetsState[facet_id] || !facetsState[facet_id].has(option_id); - } + }; function calculateFacetsState(newFacetsState: FacetState): void { setFacetsState(newFacetsState); @@ -125,10 +149,14 @@ export const FacetsPanel: React.FC = ({ facets.forEach((facet) => { const otherFacets = facets.filter((f) => f.id !== facet.id); - facetOptionQueries[facet.id] = buildCel(otherFacets, facetOptions, newFacetsState); - }) + facetOptionQueries[facet.id] = buildCel( + otherFacets, + facetOptions, + newFacetsState + ); + }); - onReloadFacetOptions && onReloadFacetOptions(facetOptionQueries) + onReloadFacetOptions && onReloadFacetOptions(facetOptionQueries); } function toggleFacetOption(facetId: string, value: string) { @@ -136,9 +164,9 @@ export const FacetsPanel: React.FC = ({ const facetState = getFacetState(facetId); if (isOptionSelected(facetId, value)) { - facetState.add(value) + facetState.add(value); } else { - facetState.delete(value) + facetState.delete(value); } calculateFacetsState({ ...facetsState, [facetId]: facetState }); @@ -148,14 +176,14 @@ export const FacetsPanel: React.FC = ({ setClickedFacetId(facetId); const facetState = getFacetState(facetId); - facetOptions[facetId].forEach(facetOption => { + facetOptions[facetId].forEach((facetOption) => { if (facetOption.display_name === optionValue) { facetState.delete(optionValue); return; } facetState.add(facetOption.display_name); - }) + }); calculateFacetsState({ ...facetsState, @@ -167,8 +195,9 @@ export const FacetsPanel: React.FC = ({ setClickedFacetId(facetId); const facetState = getFacetState(facetId); - Object.values(facetOptions[facetId]) - .forEach((option) => (facetState.delete(option.display_name))); + Object.values(facetOptions[facetId]).forEach((option) => + facetState.delete(option.display_name) + ); calculateFacetsState({ ...facetsState, @@ -180,15 +209,21 @@ export const FacetsPanel: React.FC = ({ calculateFacetsState({}); } - useEffect(function clearFiltersWhenTokenChange(): void { - if (clearFiltersToken) { - clearFilters(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [clearFiltersToken]); + useEffect( + function clearFiltersWhenTokenChange(): void { + if (clearFiltersToken) { + clearFilters(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, + [clearFiltersToken] + ); return ( -
+
{/* Facet button */} @@ -197,17 +232,17 @@ export const FacetsPanel: React.FC = ({ className="p-1 pr-2 text-sm text-gray-600 hover:bg-gray-100 rounded flex items-center gap-1" > - Add Facet + Add facet
- + {facets?.map((facet, index) => ( = ({ isStatic={facet.is_static} options={facetOptions?.[facet.id]} optionsLoading={!facetOptions?.[facet.id]} - optionsReloading={areFacetOptionsLoading && !!facet.id && clickedFacetId !== facet.id} + optionsReloading={ + areFacetOptionsLoading && + !!facet.id && + clickedFacetId !== facet.id + } onSelect={(value) => toggleFacetOption(facet.id, value)} onSelectOneOption={(value) => selectOneFacetOption(facet.id, value)} onSelectAllOptions={() => selectAllFacetOptions(facet.id)} facetState={getFacetState(facet.id)} facetKey={facet.id} - renderOptionLabel={(optionDisplayName) => renderFacetOptionLabel && renderFacetOptionLabel(facet.name, optionDisplayName)} - renderIcon={(optionDisplayName) => renderFacetOptionIcon && renderFacetOptionIcon(facet.name, optionDisplayName)} - onLoadOptions={() => onLoadFacetOptions && onLoadFacetOptions(facet.id)} + renderOptionLabel={(optionDisplayName) => + renderFacetOptionLabel && + renderFacetOptionLabel(facet.name, optionDisplayName) + } + renderIcon={(optionDisplayName) => + renderFacetOptionIcon && + renderFacetOptionIcon(facet.name, optionDisplayName) + } + onLoadOptions={() => + onLoadFacetOptions && onLoadFacetOptions(facet.id) + } onDelete={() => onDeleteFacet && onDeleteFacet(facet.id)} /> ))} @@ -232,7 +279,9 @@ export const FacetsPanel: React.FC = ({ setIsModalOpen(false)} - onAddFacet={(createFacet) => onAddFacet ? onAddFacet(createFacet) : null} + onAddFacet={(createFacet) => + onAddFacet ? onAddFacet(createFacet) : null + } />
diff --git a/keep-ui/features/incident-list/ui/incident-list.tsx b/keep-ui/features/incident-list/ui/incident-list.tsx index 5a2a1ab75..ebe8288db 100644 --- a/keep-ui/features/incident-list/ui/incident-list.tsx +++ b/keep-ui/features/incident-list/ui/incident-list.tsx @@ -25,7 +25,7 @@ import { getStatusIcon, getStatusColor } from "@/shared/lib/status-utils"; import { useUser } from "@/entities/users/model/useUser"; import { severityMapping } from "@/entities/alerts/model"; import { IncidentsNotFoundPlaceholder } from "./incidents-not-found"; -import { v4 as uuidV4 } from 'uuid'; +import { v4 as uuidV4 } from "uuid"; const AssigneeLabel = ({ email }: { email: string }) => { const user = useUser(email); @@ -81,13 +81,17 @@ export function IncidentList({ null ); - const [clearFiltersToken, setClearFiltersToken] = useState(null); - const [filterRevalidationToken, setFilterRevalidationToken] = useState(null); + const [clearFiltersToken, setClearFiltersToken] = useState( + null + ); + const [filterRevalidationToken, setFilterRevalidationToken] = useState< + string | null + >(null); const [isFormOpen, setIsFormOpen] = useState(false); useEffect(() => { setFilterRevalidationToken(incidentChangeToken); - }, [incidentChangeToken]) + }, [incidentChangeToken]); const handleCloseForm = () => { setIsFormOpen(false); @@ -129,7 +133,14 @@ export function IncidentList({ ); } if (facetName === "severity") { - return ; + return ( + + ); } if (facetName === "assignee") { return ; @@ -162,7 +173,7 @@ export function IncidentList({ const renderFacetOptionLabel = useCallback( (facetName: string, facetOptionName: string) => { facetName = facetName.toLowerCase(); - + switch (facetName) { case "assignee": if (!facetOptionName) { @@ -172,8 +183,9 @@ export function IncidentList({ case "dismissed": return facetOptionName === "true" ? "Dismissed" : "Not dismissed"; case "severity": { - const label = severityMapping[Number(facetOptionName)] || facetOptionName; - return {label}; + const label = + severityMapping[Number(facetOptionName)] || facetOptionName; + return {label}; } default: return {facetOptionName}; @@ -198,7 +210,9 @@ export function IncidentList({ if (filterCel && incidents?.items.length === 0) { return ( - setClearFiltersToken(uuidV4())} /> + setClearFiltersToken(uuidV4())} + /> ); } @@ -212,7 +226,7 @@ export function IncidentList({ } const uncheckedFacetOptionsByDefault: Record = { - "Status": ["resolved", "deleted"], + Status: ["resolved", "deleted"], }; return ( @@ -253,31 +267,29 @@ export function IncidentList({
- { - incidentsError ? ( - - ) : null - } - { - incidentsError ? null : ( -
- setFilterCel(cel)} - renderFacetOptionIcon={renderFacetOptionIcon} - renderFacetOptionLabel={renderFacetOptionLabel} - revalidationToken={filterRevalidationToken} - /> -
- {renderIncidents()} -
+ {incidentsError ? ( + + ) : null} + {incidentsError ? null : ( +
+ setFilterCel(cel)} + renderFacetOptionIcon={renderFacetOptionIcon} + renderFacetOptionLabel={renderFacetOptionLabel} + revalidationToken={filterRevalidationToken} + /> +
+ {renderIncidents()}
- ) - } +
+ )}
diff --git a/keep-ui/features/incident-list/ui/incident-table-component.tsx b/keep-ui/features/incident-list/ui/incident-table-component.tsx index 326fbe7f1..e090f83e9 100644 --- a/keep-ui/features/incident-list/ui/incident-table-component.tsx +++ b/keep-ui/features/incident-list/ui/incident-table-component.tsx @@ -21,15 +21,25 @@ interface Props { interface SortableHeaderCellProps { header: Header; children: ReactNode; + className?: string; } -const SortableHeaderCell = ({ header, children }: SortableHeaderCellProps) => { +const SortableHeaderCell = ({ + header, + children, + className, +}: SortableHeaderCellProps) => { const { column } = header; - const { style, className } = getCommonPinningStylesAndClassNames(column); + const { style, className: commonClassName } = + getCommonPinningStylesAndClassNames(column); return (
@@ -87,6 +97,7 @@ export const IncidentTableComponent = (props: Props) => { {flexRender( header.column.columnDef.header, @@ -113,6 +124,7 @@ export const IncidentTableComponent = (props: Props) => { key={cell.id} style={style} className={clsx( + cell.column.columnDef.meta?.tdClassName, className, "bg-white", cell.column.id === "actions" ? "p-1" : "" diff --git a/keep-ui/features/incident-list/ui/incidents-table.tsx b/keep-ui/features/incident-list/ui/incidents-table.tsx index 60a1240b3..27f3187bb 100644 --- a/keep-ui/features/incident-list/ui/incidents-table.tsx +++ b/keep-ui/features/incident-list/ui/incidents-table.tsx @@ -30,12 +30,13 @@ import { IncidentDropdownMenu } from "./incident-dropdown-menu"; import clsx from "clsx"; import { IncidentChangeStatusSelect } from "@/features/change-incident-status/"; import { useIncidentActions } from "@/entities/incidents/model"; -import { IncidentSeverityBadge } from "@/entities/incidents/ui"; import { getIncidentName } from "@/entities/incidents/lib/utils"; import { DateTimeField, TableIndeterminateCheckbox, TablePagination, + TableSeverityCell, + UISeverity, } from "@/shared/ui"; import { UserStatefulAvatar } from "@/entities/users/ui"; import { DynamicImageProviderIcon } from "@/components/ui"; @@ -121,6 +122,22 @@ export default function IncidentsTable({ }, [incidents.limit, incidents.offset, pagination, setPagination]); const columns = [ + columnHelper.display({ + id: "severity", + header: () => <>, + cell: ({ row }) => ( + + ), + size: 4, + minSize: 4, + maxSize: 4, + meta: { + tdClassName: "p-0", + thClassName: "p-0", + }, + }), columnHelper.display({ id: "selected", minSize: 32, @@ -179,13 +196,6 @@ export default function IncidentsTable({ id: "alerts_count", header: "Alerts", }), - columnHelper.accessor("severity", { - id: "severity", - header: "Severity", - cell: ({ row }) => ( - - ), - }), columnHelper.display({ id: "alert_sources", header: "Sources", @@ -266,7 +276,7 @@ export default function IncidentsTable({ pagination, sorting, columnPinning: { - left: ["selected"], + left: ["severity", "selected"], right: ["actions"], }, }, diff --git a/keep/api/config.py b/keep/api/config.py index f4121d0ae..41b2d05f6 100644 --- a/keep/api/config.py +++ b/keep/api/config.py @@ -1,8 +1,10 @@ import logging import os -from keep.api.alert_deduplicator.deduplication_rules_provisioning import provision_deduplication_rules_from_env import keep.api.logging +from keep.api.alert_deduplicator.deduplication_rules_provisioning import ( + provision_deduplication_rules_from_env, +) from keep.api.api import AUTH_TYPE from keep.api.core.db_on_start import migrate_db, try_create_single_tenant from keep.api.core.dependencies import SINGLE_TENANT_UUID @@ -87,3 +89,14 @@ def on_starting(server=None): os.environ["KEEP_API_URL"] = public_url logger.info("Keep server started") + + +def post_worker_init(worker): + # We need to reinitialize logging in each worker because gunicorn forks the worker processes + print("Init logging in worker") + logging.getLogger().handlers = [] # noqa + keep.api.logging.setup_logging() # noqa + print("Logging initialized in worker") + + +post_worker_init = post_worker_init diff --git a/keep/api/core/db.py b/keep/api/core/db.py index f9f42757e..ecd12676b 100644 --- a/keep/api/core/db.py +++ b/keep/api/core/db.py @@ -806,32 +806,56 @@ def push_logs_to_db(log_entries): # avoid circular import from keep.api.logging import LOG_FORMAT, LOG_FORMAT_OPEN_TELEMETRY + db_log_entries = [] if LOG_FORMAT == LOG_FORMAT_OPEN_TELEMETRY: - db_log_entries = [ - WorkflowExecutionLog( - workflow_execution_id=log_entry["workflow_execution_id"], - timestamp=datetime.strptime( - log_entry["asctime"], "%Y-%m-%d %H:%M:%S,%f" - ), - message=log_entry["message"][0:255], # limit the message to 255 chars - context=json.loads( - json.dumps(log_entry.get("context", {}), default=str) - ), # workaround to serialize any object - ) - for log_entry in log_entries - ] + for log_entry in log_entries: + try: + try: + # after formatting + message = log_entry["message"][0:255] + except Exception: + # before formatting, fallback + message = log_entry["msg"][0:255] + + try: + timestamp = datetime.strptime( + log_entry["asctime"], "%Y-%m-%d %H:%M:%S,%f" + ) + except Exception: + timestamp = log_entry["created"] + + log_entry = WorkflowExecutionLog( + workflow_execution_id=log_entry["workflow_execution_id"], + timestamp=timestamp, + message=message, + context=json.loads( + json.dumps(log_entry.get("context", {}), default=str) + ), # workaround to serialize any object + ) + db_log_entries.append(log_entry) + except Exception: + print("Failed to parse log entry - ", log_entry) + else: - db_log_entries = [ - WorkflowExecutionLog( - workflow_execution_id=log_entry["workflow_execution_id"], - timestamp=log_entry["created"], - message=log_entry["message"][0:255], # limit the message to 255 chars - context=json.loads( - json.dumps(log_entry.get("context", {}), default=str) - ), # workaround to serialize any object - ) - for log_entry in log_entries - ] + for log_entry in log_entries: + try: + try: + # after formatting + message = log_entry["message"][0:255] + except Exception: + # before formatting, fallback + message = log_entry["msg"][0:255] + log_entry = WorkflowExecutionLog( + workflow_execution_id=log_entry["workflow_execution_id"], + timestamp=log_entry["created"], + message=message, # limit the message to 255 chars + context=json.loads( + json.dumps(log_entry.get("context", {}), default=str) + ), # workaround to serialize any object + ) + db_log_entries.append(log_entry) + except Exception: + print("Failed to parse log entry - ", log_entry) # Add the LogEntry instances to the database session with Session(engine) as session: diff --git a/keep/api/logging.py b/keep/api/logging.py index 340e23ee4..4f491594d 100644 --- a/keep/api/logging.py +++ b/keep/api/logging.py @@ -43,6 +43,7 @@ def filter(self, record): if not workflow_id: return False + print("Adding workflow_id to log record") # Skip DEBUG logs unless debug mode is enabled if not getattr(thread, "workflow_debug", False) and record.levelname == "DEBUG": return False @@ -82,6 +83,7 @@ def filter(self, record): class WorkflowDBHandler(logging.Handler): def __init__(self, flush_interval: int = 2): super().__init__() + logging.getLogger(__name__).info("Initializing WorkflowDBHandler") self.records = [] self.flush_interval = flush_interval self._stop_event = threading.Event() @@ -90,11 +92,15 @@ def __init__(self, flush_interval: int = 2): self._timer_thread.daemon = ( True # Make it a daemon so it stops when program exits ) + logging.getLogger(__name__).info("Starting WorkflowDBHandler timer thread") self._timer_thread.start() + logging.getLogger(__name__).info("Started WorkflowDBHandler timer thread") def _timer_run(self): while not self._stop_event.is_set(): + # logging.getLogger(__name__).info("Timer running") self.flush() + # logging.getLogger(__name__).info("Timer sleeping") self._stop_event.wait(self.flush_interval) # Wait but can be interrupted def close(self): @@ -107,6 +113,7 @@ def emit(self, record): if not KEEP_STORE_WORKFLOW_LOGS: return if hasattr(record, "workflow_execution_id") and record.workflow_execution_id: + self.format(record) self.records.append(record) def push_logs_to_db(self): @@ -120,7 +127,9 @@ def flush(self): return try: + logging.getLogger(__name__).info("Flushing workflow logs to DB") self.push_logs_to_db() + logging.getLogger(__name__).info("Flushed workflow logs to DB") except Exception as e: # Use the parent logger to avoid infinite recursion logging.getLogger(__name__).error( @@ -326,7 +335,7 @@ def __init__(self, *args, rename_fields=None, **kwargs): }, "loggers": { "": { - "handlers": ["default", "workflowhandler"], + "handlers": ["workflowhandler", "default"], "level": "DEBUG", "propagate": False, }, @@ -452,22 +461,6 @@ def _log( ) -# MONKEY PATCHING http.client -# See: https://stackoverflow.com/questions/58738195/python-http-request-and-debug-level-logging-to-the-log-file -http_client_logger = logging.getLogger("http.client") -http_client_logger.setLevel(logging.DEBUG) -http.client.HTTPConnection.debuglevel = 1 - - -def print_to_log(*args): - http_client_logger.debug(" ".join(args)) - - -# monkey-patch a `print` global into the http.client module; all calls to -# print() in that module will then use our print_to_log implementation -http.client.print = print_to_log - - def setup_logging(): # Add file handler if KEEP_LOG_FILE is set if KEEP_LOG_FILE: @@ -482,3 +475,15 @@ def setup_logging(): CONFIG["loggers"][""]["handlers"].append("file") logging.config.dictConfig(CONFIG) + # MONKEY PATCHING http.client + # See: https://stackoverflow.com/questions/58738195/python-http-request-and-debug-level-logging-to-the-log-file + http_client_logger = logging.getLogger("http.client") + http_client_logger.setLevel(logging.DEBUG) + http.client.HTTPConnection.debuglevel = 1 + + def print_to_log(*args): + http_client_logger.debug(" ".join(args)) + + # monkey-patch a `print` global into the http.client module; all calls to + # print() in that module will then use our print_to_log implementation + http.client.print = print_to_log diff --git a/keep/providers/clickhouse_provider/clickhouse_provider.py b/keep/providers/clickhouse_provider/clickhouse_provider.py index 7974314d7..a6659c263 100644 --- a/keep/providers/clickhouse_provider/clickhouse_provider.py +++ b/keep/providers/clickhouse_provider/clickhouse_provider.py @@ -12,7 +12,7 @@ from keep.contextmanager.contextmanager import ContextManager from keep.exceptions.provider_exception import ProviderException -from keep.providers.base.base_provider import BaseProvider +from keep.providers.base.base_provider import BaseProvider, ProviderHealthMixin from keep.providers.models.provider_config import ProviderConfig, ProviderScope from keep.validation.fields import NoSchemeUrl, UrlPort @@ -66,7 +66,7 @@ class ClickhouseProviderAuthConfig: ) -class ClickhouseProvider(BaseProvider): +class ClickhouseProvider(BaseProvider, ProviderHealthMixin): """Enrich alerts with data from Clickhouse.""" PROVIDER_DISPLAY_NAME = "Clickhouse" diff --git a/keep/providers/cloudwatch_provider/cloudwatch_provider.py b/keep/providers/cloudwatch_provider/cloudwatch_provider.py index 5192c4a6e..9b0eddbf3 100644 --- a/keep/providers/cloudwatch_provider/cloudwatch_provider.py +++ b/keep/providers/cloudwatch_provider/cloudwatch_provider.py @@ -17,7 +17,7 @@ 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.base.base_provider import BaseProvider, ProviderHealthMixin from keep.providers.models.provider_config import ProviderConfig, ProviderScope @@ -60,7 +60,7 @@ class CloudwatchProviderAuthConfig: ) -class CloudwatchProvider(BaseProvider): +class CloudwatchProvider(BaseProvider, ProviderHealthMixin): """Push alarms from AWS Cloudwatch to Keep.""" PROVIDER_DISPLAY_NAME = "CloudWatch" diff --git a/keep/providers/gcpmonitoring_provider/gcpmonitoring_provider.py b/keep/providers/gcpmonitoring_provider/gcpmonitoring_provider.py index b7484fc37..57f3df42a 100644 --- a/keep/providers/gcpmonitoring_provider/gcpmonitoring_provider.py +++ b/keep/providers/gcpmonitoring_provider/gcpmonitoring_provider.py @@ -10,7 +10,7 @@ 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.base.base_provider import BaseProvider, ProviderHealthMixin from keep.providers.models.provider_config import ProviderConfig, ProviderScope from keep.providers.models.provider_method import ProviderMethod from keep.providers.providers_factory import ProvidersFactory @@ -45,7 +45,7 @@ class GcpmonitoringProviderAuthConfig: ) -class GcpmonitoringProvider(BaseProvider): +class GcpmonitoringProvider(BaseProvider, ProviderHealthMixin): """Get alerts from GCP Monitoring into Keep.""" webhook_description = "" diff --git a/keep/providers/grafana_provider/grafana_provider.py b/keep/providers/grafana_provider/grafana_provider.py index 1c5fc767e..f27fd33ef 100644 --- a/keep/providers/grafana_provider/grafana_provider.py +++ b/keep/providers/grafana_provider/grafana_provider.py @@ -15,7 +15,7 @@ from keep.api.models.db.topology import TopologyServiceInDto from keep.contextmanager.contextmanager import ContextManager from keep.exceptions.provider_exception import ProviderException -from keep.providers.base.base_provider import BaseProvider, BaseTopologyProvider +from keep.providers.base.base_provider import BaseProvider, BaseTopologyProvider, ProviderHealthMixin from keep.providers.base.provider_exceptions import GetAlertException from keep.providers.grafana_provider.grafana_alert_format_description import ( GrafanaAlertFormatDescription, @@ -58,7 +58,7 @@ class GrafanaProviderAuthConfig: ) -class GrafanaProvider(BaseTopologyProvider): +class GrafanaProvider(BaseTopologyProvider, ProviderHealthMixin): PROVIDER_DISPLAY_NAME = "Grafana" """Pull/Push alerts & Topology map from Grafana.""" diff --git a/keep/providers/incidentio_provider/incidentio_provider.py b/keep/providers/incidentio_provider/incidentio_provider.py index acd7ecb12..6ff3fb083 100644 --- a/keep/providers/incidentio_provider/incidentio_provider.py +++ b/keep/providers/incidentio_provider/incidentio_provider.py @@ -11,7 +11,7 @@ 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.base.base_provider import BaseProvider, ProviderHealthMixin from keep.providers.models.provider_config import ProviderConfig, ProviderScope @@ -36,7 +36,7 @@ class IncidentioProviderAuthConfig: ) -class IncidentioProvider(BaseProvider): +class IncidentioProvider(BaseProvider, ProviderHealthMixin): """Receive Incidents from Incidentio.""" PROVIDER_DISPLAY_NAME = "incident.io" diff --git a/keep/providers/opsgenie_provider/opsgenie_provider.py b/keep/providers/opsgenie_provider/opsgenie_provider.py index 0b6bffeb8..bfac183d7 100644 --- a/keep/providers/opsgenie_provider/opsgenie_provider.py +++ b/keep/providers/opsgenie_provider/opsgenie_provider.py @@ -7,7 +7,7 @@ from opsgenie_sdk.rest import ApiException from keep.contextmanager.contextmanager import ContextManager -from keep.providers.base.base_provider import BaseProvider +from keep.providers.base.base_provider import BaseProvider, ProviderHealthMixin from keep.providers.models.provider_config import ProviderConfig, ProviderScope from keep.providers.models.provider_method import ProviderMethod @@ -38,7 +38,7 @@ class OpsGenieRecipient(pydantic.BaseModel): id: typing.Optional[str] = None -class OpsgenieProvider(BaseProvider): +class OpsgenieProvider(BaseProvider, ProviderHealthMixin): """Create incidents in OpsGenie.""" PROVIDER_DISPLAY_NAME = "OpsGenie" diff --git a/keep/providers/pagerduty_provider/pagerduty_provider.py b/keep/providers/pagerduty_provider/pagerduty_provider.py index 5d119a79c..4ac7d44e4 100644 --- a/keep/providers/pagerduty_provider/pagerduty_provider.py +++ b/keep/providers/pagerduty_provider/pagerduty_provider.py @@ -24,7 +24,7 @@ from keep.providers.base.base_provider import ( BaseIncidentProvider, BaseProvider, - BaseTopologyProvider, + BaseTopologyProvider, ProviderHealthMixin, ) from keep.providers.models.provider_config import ProviderConfig, ProviderScope from keep.providers.providers_factory import ProvidersFactory @@ -69,7 +69,7 @@ class PagerdutyProviderAuthConfig: ) -class PagerdutyProvider(BaseTopologyProvider, BaseIncidentProvider): +class PagerdutyProvider(BaseTopologyProvider, BaseIncidentProvider, ProviderHealthMixin): """Pull alerts and query incidents from PagerDuty.""" PROVIDER_SCOPES = [ diff --git a/keep/providers/prometheus_provider/prometheus_provider.py b/keep/providers/prometheus_provider/prometheus_provider.py index b4dd4927e..4b7c760f7 100644 --- a/keep/providers/prometheus_provider/prometheus_provider.py +++ b/keep/providers/prometheus_provider/prometheus_provider.py @@ -12,7 +12,7 @@ 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.base.base_provider import BaseProvider, ProviderHealthMixin from keep.providers.models.provider_config import ProviderConfig, ProviderScope @@ -42,7 +42,7 @@ class PrometheusProviderAuthConfig: ) -class PrometheusProvider(BaseProvider): +class PrometheusProvider(BaseProvider, ProviderHealthMixin): """Get alerts from Prometheus into Keep.""" webhook_description = "This provider takes advantage of configurable webhooks available with Prometheus Alertmanager. Use the following template to configure AlertManager:"