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 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 = {};
|
||||||
|
@ -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> => {
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
|
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 { 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 (
|
||||||
|
@ -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,
|
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)
|
||||||
|
@ -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
|
||||||
|
@ -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(),
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user