mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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>
|
||||
|
20
public/app/features/alerting/unified/api/alertingApi.ts
Normal file
20
public/app/features/alerting/unified/api/alertingApi.ts
Normal 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: () => ({}),
|
||||
});
|
@ -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,27 +119,49 @@ 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) => {
|
||||
// if user config is empty for cortex alertmanager, try to get config from status endpoint
|
||||
if (
|
||||
isEmpty(result.alertmanager_config) &&
|
||||
isEmpty(result.template_files) &&
|
||||
alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME
|
||||
) {
|
||||
return fetchStatus(alertManagerSourceName).then((status) => ({
|
||||
alertmanager_config: status.config,
|
||||
template_files: {},
|
||||
template_file_provenances: result.template_file_provenances,
|
||||
}));
|
||||
}
|
||||
return result;
|
||||
});
|
||||
)
|
||||
.then((result) => {
|
||||
// if user config is empty for cortex alertmanager, try to get config from status endpoint
|
||||
if (
|
||||
isEmpty(result.alertmanager_config) &&
|
||||
isEmpty(result.template_files) &&
|
||||
alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME
|
||||
) {
|
||||
return fetchStatus(alertManagerSourceName).then((status) => ({
|
||||
alertmanager_config: status.config,
|
||||
template_files: {},
|
||||
template_file_provenances: result.template_file_provenances,
|
||||
}));
|
||||
}
|
||||
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
Block a user