mirror of https://github.com/grafana/grafana.git
				
				
				
			Alerting: Improve Mimir AM interoperability with Grafana (#53396)
This commit is contained in:
		
							parent
							
								
									932d1b6650
								
							
						
					
					
						commit
						f3085b1cac
					
				|  | @ -20,6 +20,7 @@ import teamsReducers from 'app/features/teams/state/reducers'; | |||
| import usersReducers from 'app/features/users/state/reducers'; | ||||
| import templatingReducers from 'app/features/variables/state/keyedVariablesReducer'; | ||||
| 
 | ||||
| import { alertingApi } from '../../features/alerting/unified/api/alertingApi'; | ||||
| import { CleanUp, cleanUpAction } from '../actions/cleanUp'; | ||||
| 
 | ||||
| const rootReducers = { | ||||
|  | @ -42,6 +43,7 @@ const rootReducers = { | |||
|   ...panelsReducers, | ||||
|   ...templatingReducers, | ||||
|   plugins: pluginsReducer, | ||||
|   [alertingApi.reducerPath]: alertingApi.reducer, | ||||
| }; | ||||
| 
 | ||||
| const addedReducers = {}; | ||||
|  |  | |||
|  | @ -20,6 +20,7 @@ import { AccessControlAction } from 'app/types'; | |||
| 
 | ||||
| import AmRoutes from './AmRoutes'; | ||||
| import { fetchAlertManagerConfig, fetchStatus, updateAlertManagerConfig } from './api/alertmanager'; | ||||
| import { discoverAlertmanagerFeatures } from './api/buildInfo'; | ||||
| import { mockDataSource, MockDataSourceSrv, someCloudAlertManagerConfig, someCloudAlertManagerStatus } from './mocks'; | ||||
| import { defaultGroupBy } from './utils/amroutes'; | ||||
| import { getAllDataSources } from './utils/config'; | ||||
|  | @ -29,6 +30,7 @@ import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; | |||
| jest.mock('./api/alertmanager'); | ||||
| jest.mock('./utils/config'); | ||||
| jest.mock('app/core/services/context_srv'); | ||||
| jest.mock('./api/buildInfo'); | ||||
| 
 | ||||
| const mocks = { | ||||
|   getAllDataSourcesMock: jest.mocked(getAllDataSources), | ||||
|  | @ -37,6 +39,7 @@ const mocks = { | |||
|     fetchAlertManagerConfig: jest.mocked(fetchAlertManagerConfig), | ||||
|     updateAlertManagerConfig: jest.mocked(updateAlertManagerConfig), | ||||
|     fetchStatus: jest.mocked(fetchStatus), | ||||
|     discoverAlertmanagerFeatures: jest.mocked(discoverAlertmanagerFeatures), | ||||
|   }, | ||||
|   contextSrv: jest.mocked(contextSrv), | ||||
| }; | ||||
|  | @ -83,6 +86,8 @@ const ui = { | |||
|   editButton: byRole('button', { name: 'Edit' }), | ||||
|   saveButton: byRole('button', { name: 'Save' }), | ||||
| 
 | ||||
|   setDefaultReceiverCTA: byRole('button', { name: 'Set a default contact point' }), | ||||
| 
 | ||||
|   editRouteButton: byLabelText('Edit route'), | ||||
|   deleteRouteButton: byLabelText('Delete route'), | ||||
|   newPolicyButton: byRole('button', { name: /New policy/ }), | ||||
|  | @ -192,6 +197,7 @@ describe('AmRoutes', () => { | |||
|     mocks.contextSrv.hasAccess.mockImplementation(() => true); | ||||
|     mocks.contextSrv.hasPermission.mockImplementation(() => true); | ||||
|     mocks.contextSrv.evaluatePermission.mockImplementation(() => []); | ||||
|     mocks.api.discoverAlertmanagerFeatures.mockResolvedValue({ lazyConfigInit: false }); | ||||
|     setDataSourceSrv(new MockDataSourceSrv(dataSources)); | ||||
|   }); | ||||
| 
 | ||||
|  | @ -499,7 +505,7 @@ describe('AmRoutes', () => { | |||
|     mocks.api.updateAlertManagerConfig.mockResolvedValue(Promise.resolve()); | ||||
| 
 | ||||
|     await renderAmRoutes(GRAFANA_RULES_SOURCE_NAME); | ||||
|     expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalled(); | ||||
|     await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalled()); | ||||
| 
 | ||||
|     const deleteButtons = await ui.deleteRouteButton.findAll(); | ||||
|     expect(deleteButtons).toHaveLength(1); | ||||
|  | @ -697,6 +703,21 @@ describe('AmRoutes', () => { | |||
|       }, | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   it('Shows an empty config when config returns an error and the AM supports lazy config initialization', async () => { | ||||
|     mocks.api.discoverAlertmanagerFeatures.mockResolvedValue({ lazyConfigInit: true }); | ||||
| 
 | ||||
|     mocks.api.fetchAlertManagerConfig.mockRejectedValue({ | ||||
|       message: 'alertmanager storage object not found', | ||||
|     }); | ||||
| 
 | ||||
|     await renderAmRoutes(); | ||||
| 
 | ||||
|     await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalledTimes(1)); | ||||
| 
 | ||||
|     expect(ui.rootReceiver.query()).toBeInTheDocument(); | ||||
|     expect(ui.setDefaultReceiverCTA.query()).toBeInTheDocument(); | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
| const clickSelectOption = async (selectElement: HTMLElement, optionText: string): Promise<void> => { | ||||
|  |  | |||
|  | @ -16,6 +16,7 @@ import { AccessControlAction } from 'app/types'; | |||
| 
 | ||||
| import Receivers from './Receivers'; | ||||
| import { updateAlertManagerConfig, fetchAlertManagerConfig, fetchStatus, testReceivers } from './api/alertmanager'; | ||||
| import { discoverAlertmanagerFeatures } from './api/buildInfo'; | ||||
| import { fetchNotifiers } from './api/grafana'; | ||||
| import { | ||||
|   mockDataSource, | ||||
|  | @ -33,6 +34,7 @@ jest.mock('./api/alertmanager'); | |||
| jest.mock('./api/grafana'); | ||||
| jest.mock('./utils/config'); | ||||
| jest.mock('app/core/services/context_srv'); | ||||
| jest.mock('./api/buildInfo'); | ||||
| 
 | ||||
| const mocks = { | ||||
|   getAllDataSources: jest.mocked(getAllDataSources), | ||||
|  | @ -43,6 +45,7 @@ const mocks = { | |||
|     updateConfig: jest.mocked(updateAlertManagerConfig), | ||||
|     fetchNotifiers: jest.mocked(fetchNotifiers), | ||||
|     testReceivers: jest.mocked(testReceivers), | ||||
|     discoverAlertmanagerFeatures: jest.mocked(discoverAlertmanagerFeatures), | ||||
|   }, | ||||
|   contextSrv: jest.mocked(contextSrv), | ||||
| }; | ||||
|  | @ -129,6 +132,7 @@ describe('Receivers', () => { | |||
|     jest.resetAllMocks(); | ||||
|     mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); | ||||
|     mocks.api.fetchNotifiers.mockResolvedValue(grafanaNotifiersMock); | ||||
|     mocks.api.discoverAlertmanagerFeatures.mockResolvedValue({ lazyConfigInit: false }); | ||||
|     setDataSourceSrv(new MockDataSourceSrv(dataSources)); | ||||
|     mocks.contextSrv.isEditor = true; | ||||
|     store.delete(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY); | ||||
|  | @ -470,4 +474,18 @@ describe('Receivers', () => { | |||
|     expect(mocks.api.fetchStatus).toHaveBeenCalledTimes(1); | ||||
|     expect(mocks.api.fetchStatus).toHaveBeenLastCalledWith('CloudManager'); | ||||
|   }); | ||||
| 
 | ||||
|   it('Shows an empty config when config returns an error and the AM supports lazy config initialization', async () => { | ||||
|     mocks.api.discoverAlertmanagerFeatures.mockResolvedValue({ lazyConfigInit: true }); | ||||
|     mocks.api.fetchConfig.mockRejectedValue({ message: 'alertmanager storage object not found' }); | ||||
| 
 | ||||
|     await renderReceivers('CloudManager'); | ||||
| 
 | ||||
|     const templatesTable = await ui.templatesTable.find(); | ||||
|     const receiversTable = await ui.receiversTable.find(); | ||||
| 
 | ||||
|     expect(templatesTable).toBeInTheDocument(); | ||||
|     expect(receiversTable).toBeInTheDocument(); | ||||
|     expect(ui.newContactPointButton.get()).toBeInTheDocument(); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import { Redirect, Route, RouteChildrenProps, Switch, useLocation } from 'react- | |||
| import { Alert, LoadingPlaceholder, withErrorBoundary } from '@grafana/ui'; | ||||
| import { Silence } from 'app/plugins/datasource/alertmanager/types'; | ||||
| 
 | ||||
| import { featureDiscoveryApi } from './api/featureDiscoveryApi'; | ||||
| import { AlertManagerPicker } from './components/AlertManagerPicker'; | ||||
| import { AlertingPageWrapper } from './components/AlertingPageWrapper'; | ||||
| import { NoAlertManagerWarning } from './components/NoAlertManagerWarning'; | ||||
|  | @ -31,6 +32,11 @@ const Silences: FC = () => { | |||
|   const location = useLocation(); | ||||
|   const isRoot = location.pathname.endsWith('/alerting/silences'); | ||||
| 
 | ||||
|   const { currentData: amFeatures } = featureDiscoveryApi.useDiscoverAmFeaturesQuery( | ||||
|     { amSourceName: alertManagerSourceName ?? '' }, | ||||
|     { skip: !alertManagerSourceName } | ||||
|   ); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     function fetchAll() { | ||||
|       if (alertManagerSourceName) { | ||||
|  | @ -50,6 +56,9 @@ const Silences: FC = () => { | |||
| 
 | ||||
|   const getSilenceById = useCallback((id: string) => result && result.find((silence) => silence.id === id), [result]); | ||||
| 
 | ||||
|   const mimirLazyInitError = | ||||
|     error?.message?.includes('the Alertmanager is not configured') && amFeatures?.lazyConfigInit; | ||||
| 
 | ||||
|   if (!alertManagerSourceName) { | ||||
|     return isRoot ? ( | ||||
|       <AlertingPageWrapper pageId="silences"> | ||||
|  | @ -68,12 +77,19 @@ const Silences: FC = () => { | |||
|         onChange={setAlertManagerSourceName} | ||||
|         dataSources={alertManagers} | ||||
|       /> | ||||
|       {error && !loading && ( | ||||
| 
 | ||||
|       {mimirLazyInitError && ( | ||||
|         <Alert title="The selected Alertmanager has no configuration" severity="warning"> | ||||
|           Create a new contact point to create a configuration using the default values or contact your administrator to | ||||
|           set up the Alertmanager. | ||||
|         </Alert> | ||||
|       )} | ||||
|       {error && !loading && !mimirLazyInitError && ( | ||||
|         <Alert severity="error" title="Error loading silences"> | ||||
|           {error.message || 'Unknown error.'} | ||||
|         </Alert> | ||||
|       )} | ||||
|       {alertsRequest?.error && !alertsRequest?.loading && ( | ||||
|       {alertsRequest?.error && !alertsRequest?.loading && !mimirLazyInitError && ( | ||||
|         <Alert severity="error" title="Error loading Alertmanager alerts"> | ||||
|           {alertsRequest.error?.message || 'Unknown error.'} | ||||
|         </Alert> | ||||
|  |  | |||
|  | @ -0,0 +1,20 @@ | |||
| import { BaseQueryFn, createApi } from '@reduxjs/toolkit/query/react'; | ||||
| import { lastValueFrom } from 'rxjs'; | ||||
| 
 | ||||
| import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime'; | ||||
| 
 | ||||
| const backendSrvBaseQuery = (): BaseQueryFn<BackendSrvRequest> => async (requestOptions) => { | ||||
|   try { | ||||
|     const { data, ...meta } = await lastValueFrom(getBackendSrv().fetch(requestOptions)); | ||||
| 
 | ||||
|     return { data, meta }; | ||||
|   } catch (error) { | ||||
|     return { error }; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| export const alertingApi = createApi({ | ||||
|   reducerPath: 'alertingApi', | ||||
|   baseQuery: backendSrvBaseQuery(), | ||||
|   endpoints: () => ({}), | ||||
| }); | ||||
|  | @ -1,14 +1,48 @@ | |||
| import { lastValueFrom } from 'rxjs'; | ||||
| 
 | ||||
| import { getBackendSrv, isFetchError } from '@grafana/runtime'; | ||||
| import { PromApplication, PromApiFeatures, PromBuildInfoResponse } from 'app/types/unified-alerting-dto'; | ||||
| import { | ||||
|   AlertmanagerApiFeatures, | ||||
|   PromApiFeatures, | ||||
|   PromApplication, | ||||
|   PromBuildInfoResponse, | ||||
| } from 'app/types/unified-alerting-dto'; | ||||
| 
 | ||||
| import { RULER_NOT_SUPPORTED_MSG } from '../utils/constants'; | ||||
| import { getDataSourceByName } from '../utils/datasource'; | ||||
| import { getDataSourceByName, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; | ||||
| 
 | ||||
| import { fetchRules } from './prometheus'; | ||||
| import { fetchTestRulerRulesGroup } from './ruler'; | ||||
| 
 | ||||
| /** | ||||
|  * Attempt to fetch buildinfo from our component | ||||
|  */ | ||||
| export async function discoverFeatures(dataSourceName: string): Promise<PromApiFeatures> { | ||||
|   if (dataSourceName === GRAFANA_RULES_SOURCE_NAME) { | ||||
|     return { | ||||
|       features: { | ||||
|         rulerApiEnabled: true, | ||||
|       }, | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   const dsConfig = getDataSourceByName(dataSourceName); | ||||
|   if (!dsConfig) { | ||||
|     throw new Error(`Cannot find data source configuration for ${dataSourceName}`); | ||||
|   } | ||||
| 
 | ||||
|   const { url, name, type } = dsConfig; | ||||
|   if (!url) { | ||||
|     throw new Error(`The data source url cannot be empty.`); | ||||
|   } | ||||
| 
 | ||||
|   if (type !== 'prometheus' && type !== 'loki') { | ||||
|     throw new Error(`The build info request is not available for ${type}. Only 'prometheus' and 'loki' are supported`); | ||||
|   } | ||||
| 
 | ||||
|   return discoverDataSourceFeatures({ name, url, type }); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * This function will attempt to detect what type of system we are talking to; this could be | ||||
|  * Prometheus (vanilla) | Cortex | Mimir | ||||
|  | @ -27,7 +61,7 @@ export async function discoverDataSourceFeatures(dsSettings: { | |||
|   // The current implementation of Loki's build info endpoint is useless
 | ||||
|   // because it doesn't provide information about Loki's available features (e.g. Ruler API)
 | ||||
|   // It's better to skip fetching it for Loki and go the Cortex path (manual discovery)
 | ||||
|   const buildInfoResponse = type === 'prometheus' ? await fetchPromBuildInfo(url) : undefined; | ||||
|   const buildInfoResponse = type === 'loki' ? undefined : await fetchPromBuildInfo(url); | ||||
| 
 | ||||
|   // check if the component returns buildinfo
 | ||||
|   const hasBuildInfo = buildInfoResponse !== undefined; | ||||
|  | @ -51,7 +85,7 @@ export async function discoverDataSourceFeatures(dsSettings: { | |||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   // if no features are reported but buildinfo was return we're talking to Prometheus
 | ||||
|   // if no features are reported but buildinfo was returned we're talking to Prometheus
 | ||||
|   const { features } = buildInfoResponse.data; | ||||
|   if (!features) { | ||||
|     return { | ||||
|  | @ -71,27 +105,46 @@ export async function discoverDataSourceFeatures(dsSettings: { | |||
|   }; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Attempt to fetch buildinfo from our component | ||||
|  */ | ||||
| export async function discoverFeatures(dataSourceName: string): Promise<PromApiFeatures> { | ||||
|   const dsConfig = getDataSourceByName(dataSourceName); | ||||
|   if (!dsConfig) { | ||||
|     throw new Error(`Cannot find data source configuration for ${dataSourceName}`); | ||||
| export async function discoverAlertmanagerFeatures(amSourceName: string): Promise<AlertmanagerApiFeatures> { | ||||
|   if (amSourceName === GRAFANA_RULES_SOURCE_NAME) { | ||||
|     return { lazyConfigInit: false }; | ||||
|   } | ||||
|   const { url, name, type } = dsConfig; | ||||
| 
 | ||||
|   const dsConfig = getDataSourceConfig(amSourceName); | ||||
| 
 | ||||
|   const { url, type } = dsConfig; | ||||
|   if (!url) { | ||||
|     throw new Error(`The data souce url cannot be empty.`); | ||||
|     throw new Error(`The data source url cannot be empty.`); | ||||
|   } | ||||
| 
 | ||||
|   if (type !== 'prometheus' && type !== 'loki') { | ||||
|     throw new Error(`The build info request is not available for ${type}. Only 'prometheus' and 'loki' are supported`); | ||||
|   if (type !== 'alertmanager') { | ||||
|     throw new Error( | ||||
|       `Alertmanager feature discovery is not available for ${type}. Only 'alertmanager' type is supported` | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   return discoverDataSourceFeatures({ name, url, type }); | ||||
|   return await discoverAlertmanagerFeaturesByUrl(url); | ||||
| } | ||||
| 
 | ||||
| async function fetchPromBuildInfo(url: string): Promise<PromBuildInfoResponse | undefined> { | ||||
| export async function discoverAlertmanagerFeaturesByUrl(url: string): Promise<AlertmanagerApiFeatures> { | ||||
|   try { | ||||
|     const buildInfo = await fetchPromBuildInfo(url); | ||||
|     return { lazyConfigInit: buildInfo?.data?.application === 'Grafana Mimir' }; | ||||
|   } catch (e) { | ||||
|     // If we cannot access the build info then we assume the lazy config is not available
 | ||||
|     return { lazyConfigInit: false }; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function getDataSourceConfig(amSourceName: string) { | ||||
|   const dsConfig = getDataSourceByName(amSourceName); | ||||
|   if (!dsConfig) { | ||||
|     throw new Error(`Cannot find data source configuration for ${amSourceName}`); | ||||
|   } | ||||
|   return dsConfig; | ||||
| } | ||||
| 
 | ||||
| export async function fetchPromBuildInfo(url: string): Promise<PromBuildInfoResponse | undefined> { | ||||
|   const response = await lastValueFrom( | ||||
|     getBackendSrv().fetch<PromBuildInfoResponse>({ | ||||
|       url: `${url}/api/v1/status/buildinfo`, | ||||
|  | @ -136,7 +189,6 @@ async function hasRulerSupport(dataSourceName: string) { | |||
|     throw e; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // there errors indicate that the ruler API might be disabled or not supported for Cortex
 | ||||
| function errorIndicatesMissingRulerSupport(error: any) { | ||||
|   return ( | ||||
|  |  | |||
|  | @ -0,0 +1,19 @@ | |||
| import { AlertmanagerApiFeatures } from '../../../../types/unified-alerting-dto'; | ||||
| 
 | ||||
| import { alertingApi } from './alertingApi'; | ||||
| import { discoverAlertmanagerFeatures } from './buildInfo'; | ||||
| 
 | ||||
| export const featureDiscoveryApi = alertingApi.injectEndpoints({ | ||||
|   endpoints: (build) => ({ | ||||
|     discoverAmFeatures: build.query<AlertmanagerApiFeatures, { amSourceName: string }>({ | ||||
|       queryFn: async ({ amSourceName }) => { | ||||
|         try { | ||||
|           const amFeatures = await discoverAlertmanagerFeatures(amSourceName); | ||||
|           return { data: amFeatures }; | ||||
|         } catch (error) { | ||||
|           return { error: error }; | ||||
|         } | ||||
|       }, | ||||
|     }), | ||||
|   }), | ||||
| }); | ||||
|  | @ -13,7 +13,6 @@ import { | |||
|   SilenceCreatePayload, | ||||
|   TestReceiversAlert, | ||||
| } from 'app/plugins/datasource/alertmanager/types'; | ||||
| import messageFromError from 'app/plugins/datasource/grafana-azure-monitor-datasource/utils/messageFromError'; | ||||
| import { FolderDTO, NotifierDTO, StoreState, ThunkResult } from 'app/types'; | ||||
| import { | ||||
|   CombinedRuleGroup, | ||||
|  | @ -50,6 +49,7 @@ import { | |||
| } from '../api/alertmanager'; | ||||
| import { fetchAnnotations } from '../api/annotations'; | ||||
| import { discoverFeatures } from '../api/buildInfo'; | ||||
| import { featureDiscoveryApi } from '../api/featureDiscoveryApi'; | ||||
| import { fetchNotifiers } from '../api/grafana'; | ||||
| import { FetchPromRulesFilter, fetchRules } from '../api/prometheus'; | ||||
| import { | ||||
|  | @ -69,7 +69,7 @@ import { | |||
|   isVanillaPrometheusAlertManagerDataSource, | ||||
| } from '../utils/datasource'; | ||||
| import { makeAMLink, retryWhile } from '../utils/misc'; | ||||
| import { AsyncRequestMapSlice, withAppEvents, withSerializedError } from '../utils/redux'; | ||||
| import { AsyncRequestMapSlice, messageFromError, withAppEvents, withSerializedError } from '../utils/redux'; | ||||
| import * as ruleId from '../utils/rule-id'; | ||||
| import { getRulerClient } from '../utils/rulerClient'; | ||||
| import { isRulerNotSupportedResponse } from '../utils/rules'; | ||||
|  | @ -108,7 +108,7 @@ export const fetchPromRulesAction = createAsyncThunk( | |||
| 
 | ||||
| export const fetchAlertManagerConfigAction = createAsyncThunk( | ||||
|   'unifiedalerting/fetchAmConfig', | ||||
|   (alertManagerSourceName: string): Promise<AlertManagerCortexConfig> => | ||||
|   (alertManagerSourceName: string, thunkAPI): Promise<AlertManagerCortexConfig> => | ||||
|     withSerializedError( | ||||
|       (async () => { | ||||
|         // for vanilla prometheus, there is no config endpoint. Only fetch config from status
 | ||||
|  | @ -119,13 +119,22 @@ export const fetchAlertManagerConfigAction = createAsyncThunk( | |||
|           })); | ||||
|         } | ||||
| 
 | ||||
|         const { data: amFeatures } = await thunkAPI.dispatch( | ||||
|           featureDiscoveryApi.endpoints.discoverAmFeatures.initiate({ | ||||
|             amSourceName: alertManagerSourceName, | ||||
|           }) | ||||
|         ); | ||||
| 
 | ||||
|         const lazyConfigInitSupported = amFeatures?.lazyConfigInit ?? false; | ||||
| 
 | ||||
|         return retryWhile( | ||||
|           () => fetchAlertManagerConfig(alertManagerSourceName), | ||||
|           // if config has been recently deleted, it takes a while for cortex start returning the default one.
 | ||||
|           // retry for a short while instead of failing
 | ||||
|           (e) => !!messageFromError(e)?.includes('alertmanager storage object not found'), | ||||
|           (e) => !!messageFromError(e)?.includes('alertmanager storage object not found') && !lazyConfigInitSupported, | ||||
|           FETCH_CONFIG_RETRY_TIMEOUT | ||||
|         ).then((result) => { | ||||
|         ) | ||||
|           .then((result) => { | ||||
|             // if user config is empty for cortex alertmanager, try to get config from status endpoint
 | ||||
|             if ( | ||||
|               isEmpty(result.alertmanager_config) && | ||||
|  | @ -139,6 +148,19 @@ export const fetchAlertManagerConfigAction = createAsyncThunk( | |||
|               })); | ||||
|             } | ||||
|             return result; | ||||
|           }) | ||||
|           .catch((e) => { | ||||
|             // When mimir doesn't have fallback AM url configured the default response will be as above
 | ||||
|             // However it's fine, and it's possible to create AM configuration
 | ||||
|             if (lazyConfigInitSupported && messageFromError(e)?.includes('alertmanager storage object not found')) { | ||||
|               return Promise.resolve<AlertManagerCortexConfig>({ | ||||
|                 alertmanager_config: {}, | ||||
|                 template_files: {}, | ||||
|                 template_file_provenances: {}, | ||||
|               }); | ||||
|             } | ||||
| 
 | ||||
|             throw e; | ||||
|           }); | ||||
|       })() | ||||
|     ) | ||||
|  | @ -452,7 +474,8 @@ export const updateAlertManagerConfigAction = createAsyncThunk<void, UpdateAlert | |||
|     withAppEvents( | ||||
|       withSerializedError( | ||||
|         (async () => { | ||||
|           const latestConfig = await fetchAlertManagerConfig(alertManagerSourceName); | ||||
|           const latestConfig = await thunkAPI.dispatch(fetchAlertManagerConfigAction(alertManagerSourceName)).unwrap(); | ||||
| 
 | ||||
|           if ( | ||||
|             !(isEmpty(latestConfig.alertmanager_config) && isEmpty(latestConfig.template_files)) && | ||||
|             JSON.stringify(latestConfig) !== JSON.stringify(oldConfig) | ||||
|  |  | |||
|  | @ -1,7 +1,11 @@ | |||
| import { lastValueFrom, Observable, of } from 'rxjs'; | ||||
| 
 | ||||
| import { DataQuery, DataQueryResponse, DataSourceApi, DataSourceInstanceSettings } from '@grafana/data'; | ||||
| import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime'; | ||||
| import { BackendSrvRequest, getBackendSrv, isFetchError } from '@grafana/runtime'; | ||||
| 
 | ||||
| import { discoverAlertmanagerFeaturesByUrl } from '../../../features/alerting/unified/api/buildInfo'; | ||||
| import { messageFromError } from '../../../features/alerting/unified/utils/redux'; | ||||
| import { AlertmanagerApiFeatures } from '../../../types/unified-alerting-dto'; | ||||
| 
 | ||||
| import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from './types'; | ||||
| 
 | ||||
|  | @ -43,6 +47,11 @@ export class AlertManagerDatasource extends DataSourceApi<AlertManagerQuery, Ale | |||
| 
 | ||||
|   async testDatasource() { | ||||
|     let alertmanagerResponse; | ||||
|     const amUrl = this.instanceSettings.url; | ||||
| 
 | ||||
|     const amFeatures: AlertmanagerApiFeatures = amUrl | ||||
|       ? await discoverAlertmanagerFeaturesByUrl(amUrl) | ||||
|       : { lazyConfigInit: false }; | ||||
| 
 | ||||
|     if (this.instanceSettings.jsonData.implementation === AlertManagerImplementation.prometheus) { | ||||
|       try { | ||||
|  | @ -71,7 +80,19 @@ export class AlertManagerDatasource extends DataSourceApi<AlertManagerQuery, Ale | |||
|       } catch (e) {} | ||||
|       try { | ||||
|         alertmanagerResponse = await this._request('/alertmanager/api/v2/status'); | ||||
|       } catch (e) {} | ||||
|       } catch (e) { | ||||
|         if ( | ||||
|           isFetchError(e) && | ||||
|           amFeatures.lazyConfigInit && | ||||
|           messageFromError(e)?.includes('the Alertmanager is not configured') | ||||
|         ) { | ||||
|           return { | ||||
|             status: 'success', | ||||
|             message: 'Health check passed.', | ||||
|             details: { message: 'Mimir Alertmanager without the fallback configuration has been discovered.' }, | ||||
|           }; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return alertmanagerResponse?.status === 200 | ||||
|  |  | |||
|  | @ -1,11 +1,10 @@ | |||
| import { configureStore as reduxConfigureStore, MiddlewareArray } from '@reduxjs/toolkit'; | ||||
| import { AnyAction } from 'redux'; | ||||
| import { ThunkMiddleware } from 'redux-thunk'; | ||||
| import { configureStore as reduxConfigureStore } from '@reduxjs/toolkit'; | ||||
| 
 | ||||
| import { StoreState } from 'app/types/store'; | ||||
| 
 | ||||
| import { buildInitialState } from '../core/reducers/navModel'; | ||||
| import { addReducer, createRootReducer } from '../core/reducers/root'; | ||||
| import { alertingApi } from '../features/alerting/unified/api/alertingApi'; | ||||
| 
 | ||||
| import { setStore } from './store'; | ||||
| 
 | ||||
|  | @ -17,10 +16,12 @@ export function addRootReducer(reducers: any) { | |||
| } | ||||
| 
 | ||||
| export function configureStore(initialState?: Partial<StoreState>) { | ||||
|   const store = reduxConfigureStore<StoreState, AnyAction, MiddlewareArray<[ThunkMiddleware<StoreState, AnyAction>]>>({ | ||||
|   const store = reduxConfigureStore({ | ||||
|     reducer: createRootReducer(), | ||||
|     middleware: (getDefaultMiddleware) => | ||||
|       getDefaultMiddleware({ thunk: true, serializableCheck: false, immutableCheck: false }), | ||||
|       getDefaultMiddleware({ thunk: true, serializableCheck: false, immutableCheck: false }).concat( | ||||
|         alertingApi.middleware | ||||
|       ), | ||||
|     devTools: process.env.NODE_ENV !== 'production', | ||||
|     preloadedState: { | ||||
|       navIndex: buildInitialState(), | ||||
|  |  | |||
|  | @ -78,6 +78,19 @@ export interface PromApiFeatures { | |||
|   }; | ||||
| } | ||||
| 
 | ||||
| export interface AlertmanagerApiFeatures { | ||||
|   /** | ||||
|    * Some Alertmanager implementations (Mimir) are multi-tenant systems. | ||||
|    * | ||||
|    * To save on compute costs, tenants are not active until they have a configuration set. | ||||
|    * If there is no fallback_config_file set, Alertmanager endpoints will respond with HTTP 404 | ||||
|    * | ||||
|    * Despite that, it is possible to create a configuration for such datasource | ||||
|    * by posting a new config to the `/api/v1/alerts` endpoint | ||||
|    */ | ||||
|   lazyConfigInit: boolean; | ||||
| } | ||||
| 
 | ||||
| interface PromRuleDTOBase { | ||||
|   health: string; | ||||
|   name: string; | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue