mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Refactor useExternalDataSourceAlertmanagers
(#81081)
This commit is contained in:
parent
eb889c41ee
commit
7f6806e220
@ -27,6 +27,12 @@ export const backendSrvBaseQuery = (): BaseQueryFn<BackendSrvRequest> => async (
|
||||
export const alertingApi = createApi({
|
||||
reducerPath: 'alertingApi',
|
||||
baseQuery: backendSrvBaseQuery(),
|
||||
tagTypes: ['AlertmanagerChoice', 'AlertmanagerConfiguration', 'OnCallIntegrations', 'OrgMigrationState'],
|
||||
tagTypes: [
|
||||
'AlertmanagerChoice',
|
||||
'AlertmanagerConfiguration',
|
||||
'OnCallIntegrations',
|
||||
'OrgMigrationState',
|
||||
'DataSourceSettings',
|
||||
],
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
|
15
public/app/features/alerting/unified/api/dataSourcesApi.ts
Normal file
15
public/app/features/alerting/unified/api/dataSourcesApi.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { DataSourceJsonData, DataSourceSettings } from '@grafana/data';
|
||||
|
||||
import { alertingApi } from './alertingApi';
|
||||
|
||||
export const dataSourcesApi = alertingApi.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
getAllDataSourceSettings: build.query<Array<DataSourceSettings<DataSourceJsonData>>, void>({
|
||||
query: () => ({ url: 'api/datasources' }),
|
||||
// we'll create individual cache entries for each datasource UID
|
||||
providesTags: (result) => {
|
||||
return result ? result.map(({ uid }) => ({ type: 'DataSourceSettings', id: uid })) : ['DataSourceSettings'];
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
@ -5,11 +5,11 @@ import React from 'react';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Badge, CallToActionCard, Card, Icon, LinkButton, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { ExternalDataSourceAM } from '../../hooks/useExternalAmSelector';
|
||||
import { ExternalAlertmanagerDataSourceWithStatus } from '../../hooks/useExternalAmSelector';
|
||||
import { makeDataSourceLink } from '../../utils/misc';
|
||||
|
||||
export interface ExternalAlertManagerDataSourcesProps {
|
||||
alertmanagers: ExternalDataSourceAM[];
|
||||
alertmanagers: ExternalAlertmanagerDataSourceWithStatus[];
|
||||
inactive: boolean;
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ export function ExternalAlertmanagerDataSources({ alertmanagers, inactive }: Ext
|
||||
{alertmanagers.length > 0 && (
|
||||
<div className={styles.externalDs}>
|
||||
{alertmanagers.map((am) => (
|
||||
<ExternalAMdataSourceCard key={am.dataSource.uid} alertmanager={am} inactive={inactive} />
|
||||
<ExternalAMdataSourceCard key={am.dataSourceSettings.uid} alertmanager={am} inactive={inactive} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@ -48,20 +48,20 @@ export function ExternalAlertmanagerDataSources({ alertmanagers, inactive }: Ext
|
||||
}
|
||||
|
||||
interface ExternalAMdataSourceCardProps {
|
||||
alertmanager: ExternalDataSourceAM;
|
||||
alertmanager: ExternalAlertmanagerDataSourceWithStatus;
|
||||
inactive: boolean;
|
||||
}
|
||||
|
||||
export function ExternalAMdataSourceCard({ alertmanager, inactive }: ExternalAMdataSourceCardProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const { dataSource, status, statusInconclusive, url } = alertmanager;
|
||||
const { dataSourceSettings, status } = alertmanager;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card.Heading className={styles.externalHeading}>
|
||||
{dataSource.name}{' '}
|
||||
{statusInconclusive && (
|
||||
{dataSourceSettings.name}{' '}
|
||||
{status === 'inconclusive' && (
|
||||
<Tooltip content="Multiple Alertmanagers have the same URL configured. The state might be inconclusive.">
|
||||
<Icon name="exclamation-triangle" size="md" className={styles.externalWarningIcon} />
|
||||
</Tooltip>
|
||||
@ -90,9 +90,9 @@ export function ExternalAMdataSourceCard({ alertmanager, inactive }: ExternalAMd
|
||||
/>
|
||||
)}
|
||||
</Card.Tags>
|
||||
<Card.Meta>{url}</Card.Meta>
|
||||
<Card.Meta>{dataSourceSettings.url}</Card.Meta>
|
||||
<Card.Actions>
|
||||
<LinkButton href={makeDataSourceLink(dataSource)} size="sm" variant="secondary">
|
||||
<LinkButton href={makeDataSourceLink(dataSourceSettings.uid)} size="sm" variant="secondary">
|
||||
Go to datasource
|
||||
</LinkButton>
|
||||
</Card.Actions>
|
||||
|
@ -23,6 +23,9 @@ export const ExternalAlertmanagers = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const externalDsAlertManagers = useExternalDataSourceAlertmanagers();
|
||||
const gmaHandlingAlertmanagers = externalDsAlertManagers.filter(
|
||||
(settings) => settings.dataSourceSettings.jsonData.handleGrafanaManagedAlerts === true
|
||||
);
|
||||
|
||||
const {
|
||||
useSaveExternalAlertmanagersConfigMutation,
|
||||
@ -71,7 +74,7 @@ export const ExternalAlertmanagers = () => {
|
||||
</div>
|
||||
|
||||
<ExternalAlertmanagerDataSources
|
||||
alertmanagers={externalDsAlertManagers}
|
||||
alertmanagers={gmaHandlingAlertmanagers}
|
||||
inactive={alertmanagersChoice === AlertmanagerChoice.Internal}
|
||||
/>
|
||||
</div>
|
||||
|
@ -53,7 +53,7 @@ export function RuleListErrors(): ReactElement {
|
||||
result.push(
|
||||
<>
|
||||
Failed to load the data source configuration for{' '}
|
||||
<a href={makeDataSourceLink(dataSource)} className={styles.dsLink}>
|
||||
<a href={makeDataSourceLink(dataSource.uid)} className={styles.dsLink}>
|
||||
{dataSource.name}
|
||||
</a>
|
||||
: {error.message || 'Unknown error.'}
|
||||
@ -65,7 +65,7 @@ export function RuleListErrors(): ReactElement {
|
||||
result.push(
|
||||
<>
|
||||
Failed to load rules state from{' '}
|
||||
<a href={makeDataSourceLink(dataSource)} className={styles.dsLink}>
|
||||
<a href={makeDataSourceLink(dataSource.uid)} className={styles.dsLink}>
|
||||
{dataSource.name}
|
||||
</a>
|
||||
: {error.message || 'Unknown error.'}
|
||||
@ -77,7 +77,7 @@ export function RuleListErrors(): ReactElement {
|
||||
result.push(
|
||||
<>
|
||||
Failed to load rules config from{' '}
|
||||
<a href={makeDataSourceLink(dataSource)} className={styles.dsLink}>
|
||||
<a href={makeDataSourceLink(dataSource.uid)} className={styles.dsLink}>
|
||||
{dataSource.name}
|
||||
</a>
|
||||
: {error.message || 'Unknown error.'}
|
||||
|
@ -1,16 +1,15 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { setupServer } from 'msw/node';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { rest } from 'msw';
|
||||
import { SetupServer, setupServer } from 'msw/node';
|
||||
import { TestProvider } from 'test/helpers/TestProvider';
|
||||
|
||||
import 'whatwg-fetch';
|
||||
|
||||
import { DataSourceJsonData, DataSourceSettings } from '@grafana/data';
|
||||
import { config, setBackendSrv } from '@grafana/runtime';
|
||||
import { DataSourceSettings } from '@grafana/data';
|
||||
import { setBackendSrv } from '@grafana/runtime';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { AlertManagerDataSourceJsonData } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { mockDataSource, mockDataSourcesStore, mockStore } from '../mocks';
|
||||
import { mockAlertmanagersResponse } from '../mocks/alertmanagerApi';
|
||||
|
||||
import { useExternalDataSourceAlertmanagers } from './useExternalAmSelector';
|
||||
@ -31,46 +30,42 @@ afterAll(() => {
|
||||
});
|
||||
|
||||
describe('useExternalDataSourceAlertmanagers', () => {
|
||||
it('Should merge data sources information from config and api responses', async () => {
|
||||
it('Should get the correct data source settings', async () => {
|
||||
// Arrange
|
||||
const { dsSettings, dsInstanceSettings } = setupAlertmanagerDataSource({ url: 'http://grafana.com' });
|
||||
|
||||
config.datasources = {
|
||||
'External Alertmanager': dsInstanceSettings,
|
||||
};
|
||||
|
||||
const store = mockDataSourcesStore({
|
||||
dataSources: [dsSettings],
|
||||
});
|
||||
|
||||
setupAlertmanagerDataSource(server, { url: 'http://grafana.com' });
|
||||
mockAlertmanagersResponse(server, { data: { activeAlertManagers: [], droppedAlertManagers: [] } });
|
||||
|
||||
const wrapper = ({ children }: React.PropsWithChildren<{}>) => <Provider store={store}>{children}</Provider>;
|
||||
|
||||
// Act
|
||||
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
|
||||
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper: TestProvider });
|
||||
await waitFor(() => {
|
||||
// Assert
|
||||
const { current } = result;
|
||||
|
||||
expect(current).toHaveLength(1);
|
||||
expect(current[0].dataSource.uid).toBe('1');
|
||||
expect(current[0].url).toBe('http://grafana.com');
|
||||
expect(current[0].dataSourceSettings.uid).toBe('1');
|
||||
expect(current[0].dataSourceSettings.url).toBe('http://grafana.com');
|
||||
});
|
||||
});
|
||||
|
||||
it('Should have uninterested state if data source does not want alerts', async () => {
|
||||
// Arrange
|
||||
setupAlertmanagerDataSource(server, { url: 'http://grafana.com', jsonData: { handleGrafanaManagedAlerts: false } });
|
||||
mockAlertmanagersResponse(server, { data: { activeAlertManagers: [], droppedAlertManagers: [] } });
|
||||
|
||||
// Act
|
||||
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper: TestProvider });
|
||||
await waitFor(() => {
|
||||
// Assert
|
||||
const { current } = result;
|
||||
|
||||
expect(current).toHaveLength(1);
|
||||
expect(current[0].status).toBe('uninterested');
|
||||
});
|
||||
});
|
||||
|
||||
it('Should have active state if available in the activeAlertManagers', async () => {
|
||||
// Arrange
|
||||
const { dsSettings, dsInstanceSettings } = setupAlertmanagerDataSource({ url: 'http://grafana.com' });
|
||||
|
||||
config.datasources = {
|
||||
'External Alertmanager': dsInstanceSettings,
|
||||
};
|
||||
|
||||
const store = mockStore((state) => {
|
||||
state.dataSources.dataSources = [dsSettings];
|
||||
});
|
||||
|
||||
setupAlertmanagerDataSource(server, { url: 'http://grafana.com' });
|
||||
mockAlertmanagersResponse(server, {
|
||||
data: {
|
||||
activeAlertManagers: [{ url: 'http://grafana.com/api/v2/alerts' }],
|
||||
@ -78,32 +73,20 @@ describe('useExternalDataSourceAlertmanagers', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: React.PropsWithChildren<{}>) => <Provider store={store}>{children}</Provider>;
|
||||
|
||||
// Act
|
||||
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
|
||||
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper: TestProvider });
|
||||
await waitFor(() => {
|
||||
// Assert
|
||||
const { current } = result;
|
||||
|
||||
expect(current).toHaveLength(1);
|
||||
expect(current[0].status).toBe('active');
|
||||
expect(current[0].statusInconclusive).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should have dropped state if available in the droppedAlertManagers', async () => {
|
||||
// Arrange
|
||||
const { dsSettings, dsInstanceSettings } = setupAlertmanagerDataSource({ url: 'http://grafana.com' });
|
||||
|
||||
config.datasources = {
|
||||
'External Alertmanager': dsInstanceSettings,
|
||||
};
|
||||
|
||||
const store = mockStore((state) => {
|
||||
state.dataSources.dataSources = [dsSettings];
|
||||
});
|
||||
|
||||
setupAlertmanagerDataSource(server, { url: 'http://grafana.com' });
|
||||
mockAlertmanagersResponse(server, {
|
||||
data: {
|
||||
activeAlertManagers: [],
|
||||
@ -111,10 +94,8 @@ describe('useExternalDataSourceAlertmanagers', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: React.PropsWithChildren<{}>) => <Provider store={store}>{children}</Provider>;
|
||||
|
||||
// Act
|
||||
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
|
||||
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper: TestProvider });
|
||||
|
||||
await waitFor(() => {
|
||||
// Assert
|
||||
@ -122,22 +103,12 @@ describe('useExternalDataSourceAlertmanagers', () => {
|
||||
|
||||
expect(current).toHaveLength(1);
|
||||
expect(current[0].status).toBe('dropped');
|
||||
expect(current[0].statusInconclusive).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should have pending state if not available neither in dropped nor in active alertManagers', async () => {
|
||||
// Arrange
|
||||
const { dsSettings, dsInstanceSettings } = setupAlertmanagerDataSource();
|
||||
|
||||
config.datasources = {
|
||||
'External Alertmanager': dsInstanceSettings,
|
||||
};
|
||||
|
||||
const store = mockStore((state) => {
|
||||
state.dataSources.dataSources = [dsSettings];
|
||||
});
|
||||
|
||||
setupAlertmanagerDataSource(server);
|
||||
mockAlertmanagersResponse(server, {
|
||||
data: {
|
||||
activeAlertManagers: [],
|
||||
@ -145,10 +116,8 @@ describe('useExternalDataSourceAlertmanagers', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: React.PropsWithChildren<{}>) => <Provider store={store}>{children}</Provider>;
|
||||
|
||||
// Act
|
||||
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
|
||||
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper: TestProvider });
|
||||
|
||||
await waitFor(() => {
|
||||
// Assert
|
||||
@ -156,22 +125,12 @@ describe('useExternalDataSourceAlertmanagers', () => {
|
||||
|
||||
expect(current).toHaveLength(1);
|
||||
expect(current[0].status).toBe('pending');
|
||||
expect(current[0].statusInconclusive).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should match Alertmanager url when datasource url does not have protocol specified', async () => {
|
||||
// Arrange
|
||||
const { dsSettings, dsInstanceSettings } = setupAlertmanagerDataSource({ url: 'localhost:9093' });
|
||||
|
||||
config.datasources = {
|
||||
'External Alertmanager': dsInstanceSettings,
|
||||
};
|
||||
|
||||
const store = mockStore((state) => {
|
||||
state.dataSources.dataSources = [dsSettings];
|
||||
});
|
||||
|
||||
setupAlertmanagerDataSource(server, { url: 'localhost:9093' });
|
||||
mockAlertmanagersResponse(server, {
|
||||
data: {
|
||||
activeAlertManagers: [{ url: 'http://localhost:9093/api/v2/alerts' }],
|
||||
@ -179,10 +138,8 @@ describe('useExternalDataSourceAlertmanagers', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: React.PropsWithChildren<{}>) => <Provider store={store}>{children}</Provider>;
|
||||
|
||||
// Act
|
||||
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
|
||||
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper: TestProvider });
|
||||
|
||||
await waitFor(() => {
|
||||
// Assert
|
||||
@ -190,11 +147,34 @@ describe('useExternalDataSourceAlertmanagers', () => {
|
||||
|
||||
expect(current).toHaveLength(1);
|
||||
expect(current[0].status).toBe('active');
|
||||
expect(current[0].url).toBe('localhost:9093');
|
||||
expect(current[0].dataSourceSettings.url).toBe('localhost:9093');
|
||||
});
|
||||
});
|
||||
|
||||
it('Should have inconclusive state when there are many Alertmanagers of the same URL', async () => {
|
||||
it('Should have inconclusive state when there are many Alertmanagers of the same URL on both active and inactive', async () => {
|
||||
// Arrange
|
||||
mockAlertmanagersResponse(server, {
|
||||
data: {
|
||||
activeAlertManagers: [{ url: 'http://grafana.com/api/v2/alerts' }],
|
||||
droppedAlertManagers: [{ url: 'http://grafana.com/api/v2/alerts' }],
|
||||
},
|
||||
});
|
||||
|
||||
setupAlertmanagerDataSource(server, { url: 'http://grafana.com' });
|
||||
|
||||
// Act
|
||||
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), {
|
||||
wrapper: TestProvider,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// Assert
|
||||
expect(result.current).toHaveLength(1);
|
||||
expect(result.current[0].status).toBe('inconclusive');
|
||||
});
|
||||
});
|
||||
|
||||
it('Should have not have inconclusive state when all Alertmanagers of the same URL are active', async () => {
|
||||
// Arrange
|
||||
mockAlertmanagersResponse(server, {
|
||||
data: {
|
||||
@ -203,72 +183,40 @@ describe('useExternalDataSourceAlertmanagers', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { dsSettings, dsInstanceSettings } = setupAlertmanagerDataSource({ url: 'http://grafana.com' });
|
||||
|
||||
config.datasources = {
|
||||
'External Alertmanager': dsInstanceSettings,
|
||||
};
|
||||
|
||||
const store = mockStore((state) => {
|
||||
state.dataSources.dataSources = [dsSettings];
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: React.PropsWithChildren<{}>) => <Provider store={store}>{children}</Provider>;
|
||||
setupAlertmanagerDataSource(server, { url: 'http://grafana.com' });
|
||||
|
||||
// Act
|
||||
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), {
|
||||
wrapper,
|
||||
wrapper: TestProvider,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// Assert
|
||||
expect(result.current).toHaveLength(1);
|
||||
expect(result.current[0].status).toBe('active');
|
||||
expect(result.current[0].statusInconclusive).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setupAlertmanagerDataSource(partialDsSettings?: Partial<DataSourceSettings<AlertManagerDataSourceJsonData>>) {
|
||||
function setupAlertmanagerDataSource(
|
||||
server: SetupServer,
|
||||
partialDsSettings?: Partial<DataSourceSettings<AlertManagerDataSourceJsonData>>
|
||||
) {
|
||||
const dsCommonConfig = {
|
||||
uid: '1',
|
||||
name: 'External Alertmanager',
|
||||
type: 'alertmanager',
|
||||
jsonData: { handleGrafanaManagedAlerts: true } as AlertManagerDataSourceJsonData,
|
||||
jsonData: { handleGrafanaManagedAlerts: true },
|
||||
};
|
||||
|
||||
const dsInstanceSettings = mockDataSource(dsCommonConfig);
|
||||
|
||||
const dsSettings = mockApiDataSource({
|
||||
const dsSettings = {
|
||||
...dsCommonConfig,
|
||||
...partialDsSettings,
|
||||
});
|
||||
|
||||
return { dsSettings, dsInstanceSettings };
|
||||
}
|
||||
|
||||
function mockApiDataSource(partial: Partial<DataSourceSettings<DataSourceJsonData, {}>> = {}) {
|
||||
const dsSettings: DataSourceSettings<DataSourceJsonData, {}> = {
|
||||
uid: '1',
|
||||
id: 1,
|
||||
name: '',
|
||||
url: '',
|
||||
type: '',
|
||||
access: '',
|
||||
orgId: 1,
|
||||
typeLogoUrl: '',
|
||||
typeName: '',
|
||||
user: '',
|
||||
database: '',
|
||||
basicAuth: false,
|
||||
isDefault: false,
|
||||
basicAuthUser: '',
|
||||
jsonData: { handleGrafanaManagedAlerts: true } as AlertManagerDataSourceJsonData,
|
||||
secureJsonFields: {},
|
||||
readOnly: false,
|
||||
withCredentials: false,
|
||||
...partial,
|
||||
};
|
||||
|
||||
return dsSettings;
|
||||
server.use(
|
||||
rest.get('/api/datasources', (_req, res, ctx) => {
|
||||
return res(ctx.json([dsSettings]));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -1,76 +1,114 @@
|
||||
import { countBy, keyBy } from 'lodash';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { DataSourceInstanceSettings, DataSourceJsonData, DataSourceSettings } from '@grafana/data';
|
||||
import { AlertManagerDataSourceJsonData } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { StoreState, useSelector } from 'app/types';
|
||||
import { DataSourceSettings } from '@grafana/data';
|
||||
import { AlertManagerDataSourceJsonData, ExternalAlertmanagers } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { alertmanagerApi } from '../api/alertmanagerApi';
|
||||
import { getAlertManagerDataSources } from '../utils/datasource';
|
||||
import { dataSourcesApi } from '../api/dataSourcesApi';
|
||||
import { isAlertmanagerDataSource } from '../utils/datasource';
|
||||
|
||||
export interface ExternalDataSourceAM {
|
||||
dataSource: DataSourceInstanceSettings<AlertManagerDataSourceJsonData>;
|
||||
url?: string;
|
||||
status: 'active' | 'pending' | 'dropped';
|
||||
statusInconclusive?: boolean;
|
||||
type ConnectionStatus = 'active' | 'pending' | 'dropped' | 'inconclusive' | 'uninterested' | 'unknown';
|
||||
|
||||
export interface ExternalAlertmanagerDataSourceWithStatus {
|
||||
dataSourceSettings: DataSourceSettings<AlertManagerDataSourceJsonData>;
|
||||
status: ConnectionStatus;
|
||||
}
|
||||
|
||||
export function useExternalDataSourceAlertmanagers(): ExternalDataSourceAM[] {
|
||||
const { useGetExternalAlertmanagersQuery } = alertmanagerApi;
|
||||
const { currentData: discoveredAlertmanagers } = useGetExternalAlertmanagersQuery();
|
||||
/**
|
||||
* Returns all configured Alertmanager data sources and their connection status with the internal ruler
|
||||
*/
|
||||
export function useExternalDataSourceAlertmanagers(): ExternalAlertmanagerDataSourceWithStatus[] {
|
||||
// firstly we'll fetch the settings for all datasources and filter for "alertmanager" type
|
||||
const { alertmanagerDataSources } = dataSourcesApi.endpoints.getAllDataSourceSettings.useQuery(undefined, {
|
||||
refetchOnReconnect: true,
|
||||
selectFromResult: (result) => {
|
||||
const alertmanagerDataSources = result.currentData?.filter(isAlertmanagerDataSource) ?? [];
|
||||
return { ...result, alertmanagerDataSources };
|
||||
},
|
||||
});
|
||||
|
||||
const externalDsAlertManagers = getAlertManagerDataSources().filter((ds) => ds.jsonData.handleGrafanaManagedAlerts);
|
||||
|
||||
const alertmanagerDatasources = useSelector(
|
||||
createSelector(
|
||||
(state: StoreState) => state.dataSources.dataSources.filter((ds) => ds.type === 'alertmanager'),
|
||||
(datasources) => keyBy(datasources, (ds) => ds.uid)
|
||||
)
|
||||
// we'll also fetch the configuration for which Alertmanagers we are forwarding Grafana-managed alerts too
|
||||
// @TODO use polling when we have one or more alertmanagers in pending state
|
||||
const { currentData: externalAlertmanagers } = alertmanagerApi.endpoints.getExternalAlertmanagers.useQuery(
|
||||
undefined,
|
||||
{ refetchOnReconnect: true }
|
||||
);
|
||||
|
||||
const droppedAMUrls = countBy(discoveredAlertmanagers?.droppedAlertManagers, (x) => x.url);
|
||||
const activeAMUrls = countBy(discoveredAlertmanagers?.activeAlertManagers, (x) => x.url);
|
||||
if (!alertmanagerDataSources) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return externalDsAlertManagers.map<ExternalDataSourceAM>((dsAm) => {
|
||||
const dsSettings = alertmanagerDatasources[dsAm.uid];
|
||||
|
||||
if (!dsSettings) {
|
||||
return {
|
||||
dataSource: dsAm,
|
||||
status: 'pending',
|
||||
};
|
||||
}
|
||||
|
||||
const amUrl = getDataSourceUrlWithProtocol(dsSettings);
|
||||
const amStatusUrl = `${amUrl}/api/v2/alerts`;
|
||||
|
||||
const matchingDroppedUrls = droppedAMUrls[amStatusUrl] ?? 0;
|
||||
const matchingActiveUrls = activeAMUrls[amStatusUrl] ?? 0;
|
||||
|
||||
const isDropped = matchingDroppedUrls > 0;
|
||||
const isActive = matchingActiveUrls > 0;
|
||||
|
||||
// Multiple Alertmanagers of the same URL may exist (e.g. with different credentials)
|
||||
// Alertmanager response only contains URLs, so in case of duplication, we are not able
|
||||
// to distinguish which is which, resulting in an inconclusive status.
|
||||
const isStatusInconclusive = matchingDroppedUrls + matchingActiveUrls > 1;
|
||||
|
||||
const status = isDropped ? 'dropped' : isActive ? 'active' : 'pending';
|
||||
return alertmanagerDataSources.map<ExternalAlertmanagerDataSourceWithStatus>((dataSourceSettings) => {
|
||||
const status = externalAlertmanagers
|
||||
? determineAlertmanagerConnectionStatus(externalAlertmanagers, dataSourceSettings)
|
||||
: 'unknown';
|
||||
|
||||
return {
|
||||
dataSource: dsAm,
|
||||
url: dsSettings.url,
|
||||
dataSourceSettings,
|
||||
status,
|
||||
statusInconclusive: isStatusInconclusive,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getDataSourceUrlWithProtocol<T extends DataSourceJsonData>(dsSettings: DataSourceSettings<T>) {
|
||||
const hasProtocol = new RegExp('^[^:]*://').test(dsSettings.url);
|
||||
if (!hasProtocol) {
|
||||
return `http://${dsSettings.url}`; // Grafana append http protocol if there is no any
|
||||
// using the information from /api/v1/ngalert/alertmanagers we should derive the connection status of a single data source
|
||||
function determineAlertmanagerConnectionStatus(
|
||||
externalAlertmanagers: ExternalAlertmanagers,
|
||||
dataSourceSettings: DataSourceSettings<AlertManagerDataSourceJsonData>
|
||||
): ConnectionStatus {
|
||||
const isInterestedInAlerts = dataSourceSettings.jsonData.handleGrafanaManagedAlerts;
|
||||
if (!isInterestedInAlerts) {
|
||||
return 'uninterested';
|
||||
}
|
||||
|
||||
return dsSettings.url;
|
||||
const isActive =
|
||||
externalAlertmanagers?.activeAlertManagers.some((am) => {
|
||||
return isAlertmanagerMatchByURL(dataSourceSettings.url, am.url);
|
||||
}) ?? [];
|
||||
|
||||
const isDropped =
|
||||
externalAlertmanagers?.droppedAlertManagers.some((am) => {
|
||||
return isAlertmanagerMatchByURL(dataSourceSettings.url, am.url);
|
||||
}) ?? [];
|
||||
|
||||
// the Alertmanager is being adopted (pending) if it is interested in handling alerts but not in either "active" or "dropped"
|
||||
const isPending = !isActive && !isDropped;
|
||||
if (isPending) {
|
||||
return 'pending';
|
||||
}
|
||||
|
||||
// Multiple Alertmanagers of the same URL may exist (e.g. with different credentials)
|
||||
// Alertmanager response only contains URLs, so when the URL exists in both active and dropped, we are not able
|
||||
// to distinguish which is which, resulting in an inconclusive status.
|
||||
const isInconclusive = isActive && isDropped;
|
||||
if (isInconclusive) {
|
||||
return 'inconclusive';
|
||||
}
|
||||
|
||||
// if we get here, it's neither "uninterested", nor "inconclusive" nor "pending"
|
||||
if (isActive) {
|
||||
return 'active';
|
||||
} else if (isDropped) {
|
||||
return 'dropped';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
// the vanilla Alertmanager and Mimir Alertmanager mount their API endpoints on different sub-paths
|
||||
// Cortex also uses the same paths as Mimir
|
||||
const MIMIR_ALERTMANAGER_PATH = '/alertmanager/api/v2/alerts';
|
||||
const VANILLA_ALERTMANAGER_PATH = '/api/v2/alerts';
|
||||
|
||||
// when using the Mimir Alertmanager, those paths are mounted under "/alertmanager"
|
||||
function isAlertmanagerMatchByURL(dataSourceUrl: string, alertmanagerUrl: string) {
|
||||
const normalizedUrl = normalizeDataSourceURL(dataSourceUrl);
|
||||
|
||||
const prometheusAlertmanagerMatch = alertmanagerUrl === `${normalizedUrl}${VANILLA_ALERTMANAGER_PATH}`;
|
||||
const mimirAlertmanagerMatch = alertmanagerUrl === `${normalizedUrl}${MIMIR_ALERTMANAGER_PATH}`;
|
||||
|
||||
return prometheusAlertmanagerMatch || mimirAlertmanagerMatch;
|
||||
}
|
||||
|
||||
// Grafana prepends the http protocol if there isn't one, but it doesn't store that in the datasource settings
|
||||
function normalizeDataSourceURL(url: string) {
|
||||
const hasProtocol = new RegExp('^[^:]*://').test(url);
|
||||
return hasProtocol ? url : `http://${url}`;
|
||||
}
|
||||
|
@ -602,19 +602,6 @@ export const grantUserPermissions = (permissions: AccessControlAction[]) => {
|
||||
.mockImplementation((action) => permissions.includes(action as AccessControlAction));
|
||||
};
|
||||
|
||||
export function mockDataSourcesStore(partial?: Partial<StoreState['dataSources']>) {
|
||||
const defaultState = configureStore().getState();
|
||||
const store = configureStore({
|
||||
...defaultState,
|
||||
dataSources: {
|
||||
...defaultState.dataSources,
|
||||
...partial,
|
||||
},
|
||||
});
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
export function mockUnifiedAlertingStore(unifiedAlerting?: Partial<StoreState['unifiedAlerting']>) {
|
||||
const defaultState = configureStore().getState();
|
||||
|
||||
|
@ -1,12 +1,9 @@
|
||||
import { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { AlertManagerDataSourceJsonData } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { isValidPrometheusDuration, parsePrometheusDuration } from './time';
|
||||
|
||||
export function getAllDataSources(): Array<
|
||||
DataSourceInstanceSettings<DataSourceJsonData | AlertManagerDataSourceJsonData>
|
||||
> {
|
||||
export function getAllDataSources(): Array<DataSourceInstanceSettings<DataSourceJsonData>> {
|
||||
return Object.values(config.datasources);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data';
|
||||
import { DataSourceInstanceSettings, DataSourceJsonData, DataSourceSettings } from '@grafana/data';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import {
|
||||
@ -51,12 +51,22 @@ export function getRulesDataSource(rulesSourceName: string) {
|
||||
|
||||
export function getAlertManagerDataSources() {
|
||||
return getAllDataSources()
|
||||
.filter(
|
||||
(ds): ds is DataSourceInstanceSettings<AlertManagerDataSourceJsonData> => ds.type === DataSourceType.Alertmanager
|
||||
)
|
||||
.filter(isAlertmanagerDataSourceInstance)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
export function isAlertmanagerDataSourceInstance(
|
||||
dataSource: DataSourceInstanceSettings
|
||||
): dataSource is DataSourceInstanceSettings<AlertManagerDataSourceJsonData> {
|
||||
return dataSource.type === DataSourceType.Alertmanager;
|
||||
}
|
||||
|
||||
export function isAlertmanagerDataSource(
|
||||
dataSource: DataSourceSettings
|
||||
): dataSource is DataSourceSettings<AlertManagerDataSourceJsonData> {
|
||||
return dataSource.type === DataSourceType.Alertmanager;
|
||||
}
|
||||
|
||||
export function getExternalDsAlertManagers() {
|
||||
return getAlertManagerDataSources().filter((ds) => ds.jsonData.handleGrafanaManagedAlerts);
|
||||
}
|
||||
@ -205,10 +215,9 @@ export function getDataSourceByName(name: string): DataSourceInstanceSettings<Da
|
||||
}
|
||||
|
||||
export function getAlertmanagerDataSourceByName(name: string) {
|
||||
return getAllDataSources().find(
|
||||
(source): source is DataSourceInstanceSettings<AlertManagerDataSourceJsonData> =>
|
||||
source.name === name && source.type === 'alertmanager'
|
||||
);
|
||||
return getAllDataSources()
|
||||
.filter(isAlertmanagerDataSourceInstance)
|
||||
.find((source) => source.name === name);
|
||||
}
|
||||
|
||||
export function getRulesSourceByName(name: string): RulesSource | undefined {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { sortBy } from 'lodash';
|
||||
|
||||
import { UrlQueryMap, Labels, DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data';
|
||||
import { UrlQueryMap, Labels } from '@grafana/data';
|
||||
import { GrafanaEdition } from '@grafana/data/src/types/config';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { DataSourceRef } from '@grafana/schema';
|
||||
@ -137,8 +137,8 @@ export function makeLabelBasedSilenceLink(alertManagerSourceName: string, labels
|
||||
return createUrl('/alerting/silence/new', silenceUrlParams);
|
||||
}
|
||||
|
||||
export function makeDataSourceLink<T extends DataSourceJsonData>(dataSource: DataSourceInstanceSettings<T>) {
|
||||
return createUrl(`/datasources/edit/${dataSource.uid}`);
|
||||
export function makeDataSourceLink(uid: string) {
|
||||
return createUrl(`/datasources/edit/${uid}`);
|
||||
}
|
||||
|
||||
export function makeFolderLink(folderUID: string): string {
|
||||
|
Loading…
Reference in New Issue
Block a user