Files
Virginia Cepeda f27326f7d9 Alerting: Choose a previous valid AM configuration in case of error (#65746)
* Add new property to AlertmanagerConfig type

* Implement fetching successfully applied configurations

Added method to fetch them from the API and its corresponding action and reducer

* Extract ConfigEditor as component to avoid code duplication

* Display dropdown with valid configs upon error and allow to save them

* Fix tests

* Refactor to call new endpoint using RTK

* Improve texts

* Apply suggested refactor

* Change constant casing

* Only show config selector for Grafana AM

* Remove ts-ignore

* Move code together for simplicity

* Remove invalid mock

* Update endpoint and types based on backend changes

* Rename property

* Rename alermanager config property from backend changes

* Disable editing old configurations

Due to the latest backend changes, we no longer will provide the option to edit previous AM configurations in a textearea.
Instead users will only be allowed to reset to a specific one with the same content. For this reason the textearea for old conf
igurations is disabled and a different form action (not submit) is executed on the "reset config" button. The updateAlertManage
rConfigAction is reset to its old functionality due to these changes.

* Add id to AlertManagerCortexConfig type

We'll need it to pass as a parameter to the new reset endpoint

* Add new endpoint for resetting AM configs to an old version

* Move the "Reset to selected configuration" button next to the drop-down

* Add relative offset to configurations
2023-04-05 15:13:33 -03:00

271 lines
7.9 KiB
TypeScript

import { lastValueFrom } from 'rxjs';
import { urlUtil } from '@grafana/data';
import { getBackendSrv, isFetchError } from '@grafana/runtime';
import {
AlertmanagerAlert,
AlertManagerCortexConfig,
AlertmanagerGroup,
AlertmanagerStatus,
ExternalAlertmanagerConfig,
ExternalAlertmanagersResponse,
Matcher,
Receiver,
Silence,
SilenceCreatePayload,
TestReceiversAlert,
TestReceiversPayload,
TestReceiversResult,
} from 'app/plugins/datasource/alertmanager/types';
import { getDatasourceAPIUid, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
// "grafana" for grafana-managed, otherwise a datasource name
export async function fetchAlertManagerConfig(alertManagerSourceName: string): Promise<AlertManagerCortexConfig> {
try {
const result = await lastValueFrom(
getBackendSrv().fetch<AlertManagerCortexConfig>({
url: `/api/alertmanager/${getDatasourceAPIUid(alertManagerSourceName)}/config/api/v1/alerts`,
showErrorAlert: false,
showSuccessAlert: false,
})
);
return {
template_files: result.data.template_files ?? {},
template_file_provenances: result.data.template_file_provenances ?? {},
alertmanager_config: result.data.alertmanager_config ?? {},
last_applied: result.data.last_applied,
id: result.data.id,
};
} catch (e) {
// if no config has been uploaded to grafana, it returns error instead of latest config
if (
alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME &&
isFetchError(e) &&
e.data?.message?.includes('could not find an Alertmanager configuration')
) {
return {
template_files: {},
alertmanager_config: {},
};
}
throw e;
}
}
export async function updateAlertManagerConfig(
alertManagerSourceName: string,
config: AlertManagerCortexConfig
): Promise<void> {
await lastValueFrom(
getBackendSrv().fetch({
method: 'POST',
url: `/api/alertmanager/${getDatasourceAPIUid(alertManagerSourceName)}/config/api/v1/alerts`,
data: config,
showErrorAlert: false,
showSuccessAlert: false,
})
);
}
export async function deleteAlertManagerConfig(alertManagerSourceName: string): Promise<void> {
await lastValueFrom(
getBackendSrv().fetch({
method: 'DELETE',
url: `/api/alertmanager/${getDatasourceAPIUid(alertManagerSourceName)}/config/api/v1/alerts`,
showErrorAlert: false,
showSuccessAlert: false,
})
);
}
export async function fetchSilences(alertManagerSourceName: string): Promise<Silence[]> {
const result = await lastValueFrom(
getBackendSrv().fetch<Silence[]>({
url: `/api/alertmanager/${getDatasourceAPIUid(alertManagerSourceName)}/api/v2/silences`,
showErrorAlert: false,
showSuccessAlert: false,
})
);
return result.data;
}
// returns the new silence ID. Even in the case of an update, a new silence is created and the previous one expired.
export async function createOrUpdateSilence(
alertmanagerSourceName: string,
payload: SilenceCreatePayload
): Promise<Silence> {
const result = await lastValueFrom(
getBackendSrv().fetch<Silence>({
url: `/api/alertmanager/${getDatasourceAPIUid(alertmanagerSourceName)}/api/v2/silences`,
data: payload,
showErrorAlert: false,
showSuccessAlert: false,
method: 'POST',
})
);
return result.data;
}
export async function expireSilence(alertmanagerSourceName: string, silenceID: string): Promise<void> {
await getBackendSrv().delete(
`/api/alertmanager/${getDatasourceAPIUid(alertmanagerSourceName)}/api/v2/silence/${encodeURIComponent(silenceID)}`
);
}
export async function fetchAlerts(
alertmanagerSourceName: string,
matchers?: Matcher[],
silenced = true,
active = true,
inhibited = true
): Promise<AlertmanagerAlert[]> {
const filters =
urlUtil.toUrlParams({ silenced, active, inhibited }) +
matchers
?.map(
(matcher) =>
`filter=${encodeURIComponent(
`${escapeQuotes(matcher.name)}=${matcher.isRegex ? '~' : ''}"${escapeQuotes(matcher.value)}"`
)}`
)
.join('&') || '';
const result = await lastValueFrom(
getBackendSrv().fetch<AlertmanagerAlert[]>({
url:
`/api/alertmanager/${getDatasourceAPIUid(alertmanagerSourceName)}/api/v2/alerts` +
(filters ? '?' + filters : ''),
showErrorAlert: false,
showSuccessAlert: false,
})
);
return result.data;
}
export async function fetchAlertGroups(alertmanagerSourceName: string): Promise<AlertmanagerGroup[]> {
const result = await lastValueFrom(
getBackendSrv().fetch<AlertmanagerGroup[]>({
url: `/api/alertmanager/${getDatasourceAPIUid(alertmanagerSourceName)}/api/v2/alerts/groups`,
showErrorAlert: false,
showSuccessAlert: false,
})
);
return result.data;
}
export async function fetchStatus(alertManagerSourceName: string): Promise<AlertmanagerStatus> {
const result = await lastValueFrom(
getBackendSrv().fetch<AlertmanagerStatus>({
url: `/api/alertmanager/${getDatasourceAPIUid(alertManagerSourceName)}/api/v2/status`,
showErrorAlert: false,
showSuccessAlert: false,
})
);
return result.data;
}
export async function testReceivers(
alertManagerSourceName: string,
receivers: Receiver[],
alert?: TestReceiversAlert
): Promise<void> {
const data: TestReceiversPayload = {
receivers,
alert,
};
try {
const result = await lastValueFrom(
getBackendSrv().fetch<TestReceiversResult>({
method: 'POST',
data,
url: `/api/alertmanager/${getDatasourceAPIUid(alertManagerSourceName)}/config/api/v1/receivers/test`,
showErrorAlert: false,
showSuccessAlert: false,
})
);
if (receiversResponseContainsErrors(result.data)) {
throw new Error(getReceiverResultError(result.data));
}
} catch (error) {
if (isFetchError(error) && isTestReceiversResult(error.data) && receiversResponseContainsErrors(error.data)) {
throw new Error(getReceiverResultError(error.data));
}
throw error;
}
}
function receiversResponseContainsErrors(result: TestReceiversResult) {
return result.receivers.some((receiver) =>
receiver.grafana_managed_receiver_configs.some((config) => config.status === 'failed')
);
}
function isTestReceiversResult(data: any): data is TestReceiversResult {
const receivers = data?.receivers;
if (Array.isArray(receivers)) {
return receivers.every(
(receiver: any) => typeof receiver.name === 'string' && Array.isArray(receiver.grafana_managed_receiver_configs)
);
}
return false;
}
function getReceiverResultError(receiversResult: TestReceiversResult) {
return receiversResult.receivers
.flatMap((receiver) =>
receiver.grafana_managed_receiver_configs
.filter((receiver) => receiver.status === 'failed')
.map((receiver) => receiver.error ?? 'Unknown error.')
)
.join('; ');
}
export async function addAlertManagers(alertManagerConfig: ExternalAlertmanagerConfig): Promise<void> {
await lastValueFrom(
getBackendSrv().fetch({
method: 'POST',
data: alertManagerConfig,
url: '/api/v1/ngalert/admin_config',
showErrorAlert: false,
showSuccessAlert: false,
})
).then(() => {
fetchExternalAlertmanagerConfig();
});
}
export async function fetchExternalAlertmanagers(): Promise<ExternalAlertmanagersResponse> {
const result = await lastValueFrom(
getBackendSrv().fetch<ExternalAlertmanagersResponse>({
method: 'GET',
url: '/api/v1/ngalert/alertmanagers',
})
);
return result.data;
}
export async function fetchExternalAlertmanagerConfig(): Promise<ExternalAlertmanagerConfig> {
const result = await lastValueFrom(
getBackendSrv().fetch<ExternalAlertmanagerConfig>({
method: 'GET',
url: '/api/v1/ngalert/admin_config',
showErrorAlert: false,
})
);
return result.data;
}
function escapeQuotes(value: string): string {
return value.replace(/"/g, '\\"');
}