Alerting: Improve Mimir AM interoperability with Grafana (#53396)

This commit is contained in:
Konrad Lalik 2022-08-16 16:01:57 +02:00 committed by GitHub
parent 932d1b6650
commit f3085b1cac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 254 additions and 48 deletions

View File

@ -20,6 +20,7 @@ import teamsReducers from 'app/features/teams/state/reducers';
import usersReducers from 'app/features/users/state/reducers'; import usersReducers from 'app/features/users/state/reducers';
import templatingReducers from 'app/features/variables/state/keyedVariablesReducer'; import templatingReducers from 'app/features/variables/state/keyedVariablesReducer';
import { alertingApi } from '../../features/alerting/unified/api/alertingApi';
import { CleanUp, cleanUpAction } from '../actions/cleanUp'; import { CleanUp, cleanUpAction } from '../actions/cleanUp';
const rootReducers = { const rootReducers = {
@ -42,6 +43,7 @@ const rootReducers = {
...panelsReducers, ...panelsReducers,
...templatingReducers, ...templatingReducers,
plugins: pluginsReducer, plugins: pluginsReducer,
[alertingApi.reducerPath]: alertingApi.reducer,
}; };
const addedReducers = {}; const addedReducers = {};

View File

@ -20,6 +20,7 @@ import { AccessControlAction } from 'app/types';
import AmRoutes from './AmRoutes'; import AmRoutes from './AmRoutes';
import { fetchAlertManagerConfig, fetchStatus, updateAlertManagerConfig } from './api/alertmanager'; import { fetchAlertManagerConfig, fetchStatus, updateAlertManagerConfig } from './api/alertmanager';
import { discoverAlertmanagerFeatures } from './api/buildInfo';
import { mockDataSource, MockDataSourceSrv, someCloudAlertManagerConfig, someCloudAlertManagerStatus } from './mocks'; import { mockDataSource, MockDataSourceSrv, someCloudAlertManagerConfig, someCloudAlertManagerStatus } from './mocks';
import { defaultGroupBy } from './utils/amroutes'; import { defaultGroupBy } from './utils/amroutes';
import { getAllDataSources } from './utils/config'; import { getAllDataSources } from './utils/config';
@ -29,6 +30,7 @@ import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
jest.mock('./api/alertmanager'); jest.mock('./api/alertmanager');
jest.mock('./utils/config'); jest.mock('./utils/config');
jest.mock('app/core/services/context_srv'); jest.mock('app/core/services/context_srv');
jest.mock('./api/buildInfo');
const mocks = { const mocks = {
getAllDataSourcesMock: jest.mocked(getAllDataSources), getAllDataSourcesMock: jest.mocked(getAllDataSources),
@ -37,6 +39,7 @@ const mocks = {
fetchAlertManagerConfig: jest.mocked(fetchAlertManagerConfig), fetchAlertManagerConfig: jest.mocked(fetchAlertManagerConfig),
updateAlertManagerConfig: jest.mocked(updateAlertManagerConfig), updateAlertManagerConfig: jest.mocked(updateAlertManagerConfig),
fetchStatus: jest.mocked(fetchStatus), fetchStatus: jest.mocked(fetchStatus),
discoverAlertmanagerFeatures: jest.mocked(discoverAlertmanagerFeatures),
}, },
contextSrv: jest.mocked(contextSrv), contextSrv: jest.mocked(contextSrv),
}; };
@ -83,6 +86,8 @@ const ui = {
editButton: byRole('button', { name: 'Edit' }), editButton: byRole('button', { name: 'Edit' }),
saveButton: byRole('button', { name: 'Save' }), saveButton: byRole('button', { name: 'Save' }),
setDefaultReceiverCTA: byRole('button', { name: 'Set a default contact point' }),
editRouteButton: byLabelText('Edit route'), editRouteButton: byLabelText('Edit route'),
deleteRouteButton: byLabelText('Delete route'), deleteRouteButton: byLabelText('Delete route'),
newPolicyButton: byRole('button', { name: /New policy/ }), newPolicyButton: byRole('button', { name: /New policy/ }),
@ -192,6 +197,7 @@ describe('AmRoutes', () => {
mocks.contextSrv.hasAccess.mockImplementation(() => true); mocks.contextSrv.hasAccess.mockImplementation(() => true);
mocks.contextSrv.hasPermission.mockImplementation(() => true); mocks.contextSrv.hasPermission.mockImplementation(() => true);
mocks.contextSrv.evaluatePermission.mockImplementation(() => []); mocks.contextSrv.evaluatePermission.mockImplementation(() => []);
mocks.api.discoverAlertmanagerFeatures.mockResolvedValue({ lazyConfigInit: false });
setDataSourceSrv(new MockDataSourceSrv(dataSources)); setDataSourceSrv(new MockDataSourceSrv(dataSources));
}); });
@ -499,7 +505,7 @@ describe('AmRoutes', () => {
mocks.api.updateAlertManagerConfig.mockResolvedValue(Promise.resolve()); mocks.api.updateAlertManagerConfig.mockResolvedValue(Promise.resolve());
await renderAmRoutes(GRAFANA_RULES_SOURCE_NAME); await renderAmRoutes(GRAFANA_RULES_SOURCE_NAME);
expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalled(); await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalled());
const deleteButtons = await ui.deleteRouteButton.findAll(); const deleteButtons = await ui.deleteRouteButton.findAll();
expect(deleteButtons).toHaveLength(1); 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> => { const clickSelectOption = async (selectElement: HTMLElement, optionText: string): Promise<void> => {

View File

@ -16,6 +16,7 @@ import { AccessControlAction } from 'app/types';
import Receivers from './Receivers'; import Receivers from './Receivers';
import { updateAlertManagerConfig, fetchAlertManagerConfig, fetchStatus, testReceivers } from './api/alertmanager'; import { updateAlertManagerConfig, fetchAlertManagerConfig, fetchStatus, testReceivers } from './api/alertmanager';
import { discoverAlertmanagerFeatures } from './api/buildInfo';
import { fetchNotifiers } from './api/grafana'; import { fetchNotifiers } from './api/grafana';
import { import {
mockDataSource, mockDataSource,
@ -33,6 +34,7 @@ jest.mock('./api/alertmanager');
jest.mock('./api/grafana'); jest.mock('./api/grafana');
jest.mock('./utils/config'); jest.mock('./utils/config');
jest.mock('app/core/services/context_srv'); jest.mock('app/core/services/context_srv');
jest.mock('./api/buildInfo');
const mocks = { const mocks = {
getAllDataSources: jest.mocked(getAllDataSources), getAllDataSources: jest.mocked(getAllDataSources),
@ -43,6 +45,7 @@ const mocks = {
updateConfig: jest.mocked(updateAlertManagerConfig), updateConfig: jest.mocked(updateAlertManagerConfig),
fetchNotifiers: jest.mocked(fetchNotifiers), fetchNotifiers: jest.mocked(fetchNotifiers),
testReceivers: jest.mocked(testReceivers), testReceivers: jest.mocked(testReceivers),
discoverAlertmanagerFeatures: jest.mocked(discoverAlertmanagerFeatures),
}, },
contextSrv: jest.mocked(contextSrv), contextSrv: jest.mocked(contextSrv),
}; };
@ -129,6 +132,7 @@ describe('Receivers', () => {
jest.resetAllMocks(); jest.resetAllMocks();
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
mocks.api.fetchNotifiers.mockResolvedValue(grafanaNotifiersMock); mocks.api.fetchNotifiers.mockResolvedValue(grafanaNotifiersMock);
mocks.api.discoverAlertmanagerFeatures.mockResolvedValue({ lazyConfigInit: false });
setDataSourceSrv(new MockDataSourceSrv(dataSources)); setDataSourceSrv(new MockDataSourceSrv(dataSources));
mocks.contextSrv.isEditor = true; mocks.contextSrv.isEditor = true;
store.delete(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY); store.delete(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY);
@ -470,4 +474,18 @@ describe('Receivers', () => {
expect(mocks.api.fetchStatus).toHaveBeenCalledTimes(1); expect(mocks.api.fetchStatus).toHaveBeenCalledTimes(1);
expect(mocks.api.fetchStatus).toHaveBeenLastCalledWith('CloudManager'); 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();
});
}); });

View File

@ -5,6 +5,7 @@ import { Redirect, Route, RouteChildrenProps, Switch, useLocation } from 'react-
import { Alert, LoadingPlaceholder, withErrorBoundary } from '@grafana/ui'; import { Alert, LoadingPlaceholder, withErrorBoundary } from '@grafana/ui';
import { Silence } from 'app/plugins/datasource/alertmanager/types'; import { Silence } from 'app/plugins/datasource/alertmanager/types';
import { featureDiscoveryApi } from './api/featureDiscoveryApi';
import { AlertManagerPicker } from './components/AlertManagerPicker'; import { AlertManagerPicker } from './components/AlertManagerPicker';
import { AlertingPageWrapper } from './components/AlertingPageWrapper'; import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import { NoAlertManagerWarning } from './components/NoAlertManagerWarning'; import { NoAlertManagerWarning } from './components/NoAlertManagerWarning';
@ -31,6 +32,11 @@ const Silences: FC = () => {
const location = useLocation(); const location = useLocation();
const isRoot = location.pathname.endsWith('/alerting/silences'); const isRoot = location.pathname.endsWith('/alerting/silences');
const { currentData: amFeatures } = featureDiscoveryApi.useDiscoverAmFeaturesQuery(
{ amSourceName: alertManagerSourceName ?? '' },
{ skip: !alertManagerSourceName }
);
useEffect(() => { useEffect(() => {
function fetchAll() { function fetchAll() {
if (alertManagerSourceName) { if (alertManagerSourceName) {
@ -50,6 +56,9 @@ const Silences: FC = () => {
const getSilenceById = useCallback((id: string) => result && result.find((silence) => silence.id === id), [result]); 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) { if (!alertManagerSourceName) {
return isRoot ? ( return isRoot ? (
<AlertingPageWrapper pageId="silences"> <AlertingPageWrapper pageId="silences">
@ -68,12 +77,19 @@ const Silences: FC = () => {
onChange={setAlertManagerSourceName} onChange={setAlertManagerSourceName}
dataSources={alertManagers} 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"> <Alert severity="error" title="Error loading silences">
{error.message || 'Unknown error.'} {error.message || 'Unknown error.'}
</Alert> </Alert>
)} )}
{alertsRequest?.error && !alertsRequest?.loading && ( {alertsRequest?.error && !alertsRequest?.loading && !mimirLazyInitError && (
<Alert severity="error" title="Error loading Alertmanager alerts"> <Alert severity="error" title="Error loading Alertmanager alerts">
{alertsRequest.error?.message || 'Unknown error.'} {alertsRequest.error?.message || 'Unknown error.'}
</Alert> </Alert>

View File

@ -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: () => ({}),
});

View File

@ -1,14 +1,48 @@
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { getBackendSrv, isFetchError } from '@grafana/runtime'; 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 { 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 { fetchRules } from './prometheus';
import { fetchTestRulerRulesGroup } from './ruler'; 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 * This function will attempt to detect what type of system we are talking to; this could be
* Prometheus (vanilla) | Cortex | Mimir * Prometheus (vanilla) | Cortex | Mimir
@ -27,7 +61,7 @@ export async function discoverDataSourceFeatures(dsSettings: {
// The current implementation of Loki's build info endpoint is useless // 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) // 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) // 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 // check if the component returns buildinfo
const hasBuildInfo = buildInfoResponse !== undefined; 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; const { features } = buildInfoResponse.data;
if (!features) { if (!features) {
return { return {
@ -71,27 +105,46 @@ export async function discoverDataSourceFeatures(dsSettings: {
}; };
} }
/** export async function discoverAlertmanagerFeatures(amSourceName: string): Promise<AlertmanagerApiFeatures> {
* Attempt to fetch buildinfo from our component if (amSourceName === GRAFANA_RULES_SOURCE_NAME) {
*/ return { lazyConfigInit: false };
export async function discoverFeatures(dataSourceName: string): Promise<PromApiFeatures> {
const dsConfig = getDataSourceByName(dataSourceName);
if (!dsConfig) {
throw new Error(`Cannot find data source configuration for ${dataSourceName}`);
} }
const { url, name, type } = dsConfig;
const dsConfig = getDataSourceConfig(amSourceName);
const { url, type } = dsConfig;
if (!url) { 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') { if (type !== 'alertmanager') {
throw new Error(`The build info request is not available for ${type}. Only 'prometheus' and 'loki' are supported`); 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( const response = await lastValueFrom(
getBackendSrv().fetch<PromBuildInfoResponse>({ getBackendSrv().fetch<PromBuildInfoResponse>({
url: `${url}/api/v1/status/buildinfo`, url: `${url}/api/v1/status/buildinfo`,
@ -136,7 +189,6 @@ async function hasRulerSupport(dataSourceName: string) {
throw e; throw e;
} }
} }
// there errors indicate that the ruler API might be disabled or not supported for Cortex // there errors indicate that the ruler API might be disabled or not supported for Cortex
function errorIndicatesMissingRulerSupport(error: any) { function errorIndicatesMissingRulerSupport(error: any) {
return ( return (

View File

@ -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 };
}
},
}),
}),
});

View File

@ -13,7 +13,6 @@ import {
SilenceCreatePayload, SilenceCreatePayload,
TestReceiversAlert, TestReceiversAlert,
} from 'app/plugins/datasource/alertmanager/types'; } 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 { FolderDTO, NotifierDTO, StoreState, ThunkResult } from 'app/types';
import { import {
CombinedRuleGroup, CombinedRuleGroup,
@ -50,6 +49,7 @@ import {
} from '../api/alertmanager'; } from '../api/alertmanager';
import { fetchAnnotations } from '../api/annotations'; import { fetchAnnotations } from '../api/annotations';
import { discoverFeatures } from '../api/buildInfo'; import { discoverFeatures } from '../api/buildInfo';
import { featureDiscoveryApi } from '../api/featureDiscoveryApi';
import { fetchNotifiers } from '../api/grafana'; import { fetchNotifiers } from '../api/grafana';
import { FetchPromRulesFilter, fetchRules } from '../api/prometheus'; import { FetchPromRulesFilter, fetchRules } from '../api/prometheus';
import { import {
@ -69,7 +69,7 @@ import {
isVanillaPrometheusAlertManagerDataSource, isVanillaPrometheusAlertManagerDataSource,
} from '../utils/datasource'; } from '../utils/datasource';
import { makeAMLink, retryWhile } from '../utils/misc'; 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 * as ruleId from '../utils/rule-id';
import { getRulerClient } from '../utils/rulerClient'; import { getRulerClient } from '../utils/rulerClient';
import { isRulerNotSupportedResponse } from '../utils/rules'; import { isRulerNotSupportedResponse } from '../utils/rules';
@ -108,7 +108,7 @@ export const fetchPromRulesAction = createAsyncThunk(
export const fetchAlertManagerConfigAction = createAsyncThunk( export const fetchAlertManagerConfigAction = createAsyncThunk(
'unifiedalerting/fetchAmConfig', 'unifiedalerting/fetchAmConfig',
(alertManagerSourceName: string): Promise<AlertManagerCortexConfig> => (alertManagerSourceName: string, thunkAPI): Promise<AlertManagerCortexConfig> =>
withSerializedError( withSerializedError(
(async () => { (async () => {
// for vanilla prometheus, there is no config endpoint. Only fetch config from status // 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( return retryWhile(
() => fetchAlertManagerConfig(alertManagerSourceName), () => fetchAlertManagerConfig(alertManagerSourceName),
// if config has been recently deleted, it takes a while for cortex start returning the default one. // 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 // 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 FETCH_CONFIG_RETRY_TIMEOUT
).then((result) => { )
.then((result) => {
// if user config is empty for cortex alertmanager, try to get config from status endpoint // if user config is empty for cortex alertmanager, try to get config from status endpoint
if ( if (
isEmpty(result.alertmanager_config) && isEmpty(result.alertmanager_config) &&
@ -139,6 +148,19 @@ export const fetchAlertManagerConfigAction = createAsyncThunk(
})); }));
} }
return result; 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( withAppEvents(
withSerializedError( withSerializedError(
(async () => { (async () => {
const latestConfig = await fetchAlertManagerConfig(alertManagerSourceName); const latestConfig = await thunkAPI.dispatch(fetchAlertManagerConfigAction(alertManagerSourceName)).unwrap();
if ( if (
!(isEmpty(latestConfig.alertmanager_config) && isEmpty(latestConfig.template_files)) && !(isEmpty(latestConfig.alertmanager_config) && isEmpty(latestConfig.template_files)) &&
JSON.stringify(latestConfig) !== JSON.stringify(oldConfig) JSON.stringify(latestConfig) !== JSON.stringify(oldConfig)

View File

@ -1,7 +1,11 @@
import { lastValueFrom, Observable, of } from 'rxjs'; import { lastValueFrom, Observable, of } from 'rxjs';
import { DataQuery, DataQueryResponse, DataSourceApi, DataSourceInstanceSettings } from '@grafana/data'; 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'; import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from './types';
@ -43,6 +47,11 @@ export class AlertManagerDatasource extends DataSourceApi<AlertManagerQuery, Ale
async testDatasource() { async testDatasource() {
let alertmanagerResponse; let alertmanagerResponse;
const amUrl = this.instanceSettings.url;
const amFeatures: AlertmanagerApiFeatures = amUrl
? await discoverAlertmanagerFeaturesByUrl(amUrl)
: { lazyConfigInit: false };
if (this.instanceSettings.jsonData.implementation === AlertManagerImplementation.prometheus) { if (this.instanceSettings.jsonData.implementation === AlertManagerImplementation.prometheus) {
try { try {
@ -71,7 +80,19 @@ export class AlertManagerDatasource extends DataSourceApi<AlertManagerQuery, Ale
} catch (e) {} } catch (e) {}
try { try {
alertmanagerResponse = await this._request('/alertmanager/api/v2/status'); 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 return alertmanagerResponse?.status === 200

View File

@ -1,11 +1,10 @@
import { configureStore as reduxConfigureStore, MiddlewareArray } from '@reduxjs/toolkit'; import { configureStore as reduxConfigureStore } from '@reduxjs/toolkit';
import { AnyAction } from 'redux';
import { ThunkMiddleware } from 'redux-thunk';
import { StoreState } from 'app/types/store'; import { StoreState } from 'app/types/store';
import { buildInitialState } from '../core/reducers/navModel'; import { buildInitialState } from '../core/reducers/navModel';
import { addReducer, createRootReducer } from '../core/reducers/root'; import { addReducer, createRootReducer } from '../core/reducers/root';
import { alertingApi } from '../features/alerting/unified/api/alertingApi';
import { setStore } from './store'; import { setStore } from './store';
@ -17,10 +16,12 @@ export function addRootReducer(reducers: any) {
} }
export function configureStore(initialState?: Partial<StoreState>) { export function configureStore(initialState?: Partial<StoreState>) {
const store = reduxConfigureStore<StoreState, AnyAction, MiddlewareArray<[ThunkMiddleware<StoreState, AnyAction>]>>({ const store = reduxConfigureStore({
reducer: createRootReducer(), reducer: createRootReducer(),
middleware: (getDefaultMiddleware) => 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', devTools: process.env.NODE_ENV !== 'production',
preloadedState: { preloadedState: {
navIndex: buildInitialState(), navIndex: buildInitialState(),

View File

@ -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 { interface PromRuleDTOBase {
health: string; health: string;
name: string; name: string;