import { DataSourceSettings } from '@grafana/data'; import { AlertManagerDataSourceJsonData, ExternalAlertmanagersConnectionStatus, } from 'app/plugins/datasource/alertmanager/types'; import { alertmanagerApi } from '../api/alertmanagerApi'; import { dataSourcesApi } from '../api/dataSourcesApi'; import { isAlertmanagerDataSource } from '../utils/datasource'; export type ConnectionStatus = 'active' | 'pending' | 'dropped' | 'inconclusive' | 'uninterested' | 'unknown'; export interface ExternalAlertmanagerDataSourceWithStatus { dataSourceSettings: DataSourceSettings; status: ConnectionStatus; } interface UseExternalDataSourceAlertmanagersProps { refetchOnMountOrArgChange?: boolean; } /** * Returns all configured Alertmanager data sources and their connection status with the internal ruler */ export function useExternalDataSourceAlertmanagers({ refetchOnMountOrArgChange = false, }: UseExternalDataSourceAlertmanagersProps = {}): ExternalAlertmanagerDataSourceWithStatus[] { // firstly we'll fetch the settings for all datasources and filter for "alertmanager" type const { alertmanagerDataSources } = dataSourcesApi.endpoints.getAllDataSourceSettings.useQuery(undefined, { refetchOnReconnect: true, // we will refetch the list of data sources every time the component is rendered so we always show fresh data after a user // may have made changes to a data source and came back to the list refetchOnMountOrArgChange, selectFromResult: (result) => { const alertmanagerDataSources = result.currentData?.filter(isAlertmanagerDataSource) ?? []; return { ...result, alertmanagerDataSources }; }, }); // we'll also fetch the configuration for which Alertmanagers we are forwarding Grafana-managed alerts too const { currentData: externalAlertmanagers } = alertmanagerApi.endpoints.getExternalAlertmanagers.useQuery( undefined, { refetchOnReconnect: true, refetchOnMountOrArgChange } ); if (!alertmanagerDataSources) { return []; } return alertmanagerDataSources.map((dataSourceSettings) => { const status = externalAlertmanagers ? determineAlertmanagerConnectionStatus(externalAlertmanagers, dataSourceSettings) : 'unknown'; return { dataSourceSettings, status, }; }); } // using the information from /api/v1/ngalert/alertmanagers we should derive the connection status of a single data source function determineAlertmanagerConnectionStatus( externalAlertmanagers: ExternalAlertmanagersConnectionStatus, dataSourceSettings: DataSourceSettings ): ConnectionStatus { const isInterestedInAlerts = dataSourceSettings.jsonData.handleGrafanaManagedAlerts; if (!isInterestedInAlerts) { return 'uninterested'; } const isActive = externalAlertmanagers?.activeAlertManagers.some((am) => { return isAlertmanagerMatchByURL(dataSourceSettings.url, am.url); }) ?? []; const isDropped = externalAlertmanagers?.droppedAlertManagers.some((am) => { return isAlertmanagerMatchByURL(dataSourceSettings.url, am.url); }) ?? []; // the Alertmanager is being adopted (pending) if it is interested in handling alerts but not in either "active" or "dropped" const isPending = !isActive && !isDropped; if (isPending) { return 'pending'; } // Multiple Alertmanagers of the same URL may exist (e.g. with different credentials) // Alertmanager response only contains URLs, so when the URL exists in both active and dropped, we are not able // to distinguish which is which, resulting in an inconclusive status. const isInconclusive = isActive && isDropped; if (isInconclusive) { return 'inconclusive'; } // if we get here, it's neither "uninterested", nor "inconclusive" nor "pending" if (isActive) { return 'active'; } else if (isDropped) { return 'dropped'; } return 'unknown'; } // the vanilla Alertmanager and Mimir Alertmanager mount their API endpoints on different sub-paths // Cortex also uses the same paths as Mimir const MIMIR_ALERTMANAGER_PATH = '/alertmanager/api/v2/alerts'; const VANILLA_ALERTMANAGER_PATH = '/api/v2/alerts'; // when using the Mimir Alertmanager, those paths are mounted under "/alertmanager" function isAlertmanagerMatchByURL(dataSourceUrl: string, alertmanagerUrl: string) { const normalizedUrl = normalizeDataSourceURL(dataSourceUrl); const prometheusAlertmanagerMatch = alertmanagerUrl === `${normalizedUrl}${VANILLA_ALERTMANAGER_PATH}`; const mimirAlertmanagerMatch = alertmanagerUrl === `${normalizedUrl}${MIMIR_ALERTMANAGER_PATH}`; return prometheusAlertmanagerMatch || mimirAlertmanagerMatch; } // Grafana prepends the http protocol if there isn't one, but it doesn't store that in the datasource settings export function normalizeDataSourceURL(url: string) { const hasProtocol = new RegExp('^[^:]*://').test(url); const urlWithProtocol = hasProtocol ? url : `http://${url}`; // replace trailing slashes return urlWithProtocol.replace(/\/+$/, ''); }