import { lastValueFrom } from 'rxjs'; import { FetchResponse, getBackendSrv } from '@grafana/runtime'; import { RulerDataSourceConfig } from 'app/types/unified-alerting'; import { PostableRulerRuleGroupDTO, RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto'; import { RULER_NOT_SUPPORTED_MSG } from '../utils/constants'; import { getDatasourceAPIUid, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; import { prepareRulesFilterQueryParams } from './prometheus'; interface ErrorResponseMessage { message?: string; error?: string; } export interface RulerRequestUrl { path: string; params?: Record; } export function rulerUrlBuilder(rulerConfig: RulerDataSourceConfig) { const grafanaServerPath = `/api/ruler/${getDatasourceAPIUid(rulerConfig.dataSourceName)}`; const rulerPath = `${grafanaServerPath}/api/v1/rules`; const rulerSearchParams = new URLSearchParams(); rulerSearchParams.set('subtype', rulerConfig.apiVersion === 'legacy' ? 'cortex' : 'mimir'); return { rules: (filter?: FetchRulerRulesFilter): RulerRequestUrl => { const params = prepareRulesFilterQueryParams(rulerSearchParams, filter); return { path: `${rulerPath}`, params: params, }; }, namespace: (namespace: string): RulerRequestUrl => ({ path: `${rulerPath}/${encodeURIComponent(namespace)}`, params: Object.fromEntries(rulerSearchParams), }), namespaceGroup: (namespace: string, group: string): RulerRequestUrl => ({ path: `${rulerPath}/${encodeURIComponent(namespace)}/${encodeURIComponent(group)}`, params: Object.fromEntries(rulerSearchParams), }), }; } // upsert a rule group. use this to update rule export async function setRulerRuleGroup( rulerConfig: RulerDataSourceConfig, namespace: string, group: PostableRulerRuleGroupDTO ): Promise { const { path, params } = rulerUrlBuilder(rulerConfig).namespace(namespace); await lastValueFrom( getBackendSrv().fetch({ method: 'POST', url: path, data: group, showErrorAlert: false, showSuccessAlert: false, params, }) ); } export interface FetchRulerRulesFilter { dashboardUID: string; panelId?: number; } // fetch all ruler rule namespaces and included groups export async function fetchRulerRules(rulerConfig: RulerDataSourceConfig, filter?: FetchRulerRulesFilter) { if (filter?.dashboardUID && rulerConfig.dataSourceName !== GRAFANA_RULES_SOURCE_NAME) { throw new Error('Filtering by dashboard UID is only supported by Grafana.'); } // TODO Move params creation to the rules function const { path: url, params } = rulerUrlBuilder(rulerConfig).rules(filter); return rulerGetRequest(url, {}, params); } // fetch rule groups for a particular namespace // will throw with { status: 404 } if namespace does not exist export async function fetchRulerRulesNamespace(rulerConfig: RulerDataSourceConfig, namespace: string) { const { path, params } = rulerUrlBuilder(rulerConfig).namespace(namespace); const result = await rulerGetRequest>(path, {}, params); return result[namespace] || []; } // fetch a particular rule group // will throw with { status: 404 } if rule group does not exist export async function fetchTestRulerRulesGroup(dataSourceName: string): Promise { return rulerGetRequest( `/api/ruler/${getDatasourceAPIUid(dataSourceName)}/api/v1/rules/test/test`, null ); } export async function fetchRulerRulesGroup( rulerConfig: RulerDataSourceConfig, namespace: string, group: string ): Promise { const { path, params } = rulerUrlBuilder(rulerConfig).namespaceGroup(namespace, group); return rulerGetRequest(path, null, params); } export async function deleteRulerRulesGroup(rulerConfig: RulerDataSourceConfig, namespace: string, groupName: string) { const { path, params } = rulerUrlBuilder(rulerConfig).namespaceGroup(namespace, groupName); await lastValueFrom( getBackendSrv().fetch({ url: path, method: 'DELETE', showSuccessAlert: false, showErrorAlert: false, params, }) ); } // false in case ruler is not supported. this is weird, but we'll work on it async function rulerGetRequest(url: string, empty: T, params?: Record): Promise { try { const response = await lastValueFrom( getBackendSrv().fetch({ url, showErrorAlert: false, showSuccessAlert: false, params, }) ); return response.data; } catch (error) { if (!isResponseError(error)) { throw error; } if (isCortexErrorResponse(error)) { return empty; } else if (isRulerNotSupported(error)) { // assert if the endoint is not supported at all throw { ...error, data: { ...error.data, message: RULER_NOT_SUPPORTED_MSG, }, }; } throw error; } } function isResponseError(error: unknown): error is FetchResponse { const hasErrorMessage = (error as FetchResponse).data != null; const hasErrorCode = Number.isFinite((error as FetchResponse).status); return hasErrorCode && hasErrorMessage; } function isRulerNotSupported(error: FetchResponse) { return ( error.status === 404 || (error.status === 500 && error.data.message?.includes('unexpected content type from upstream. expected YAML, got text/html')) ); } function isCortexErrorResponse(error: FetchResponse) { return ( error.status === 404 && (error.data.message?.includes('group does not exist') || error.data.message?.includes('no rule groups found')) ); } export async function deleteNamespace(rulerConfig: RulerDataSourceConfig, namespace: string): Promise { const { path, params } = rulerUrlBuilder(rulerConfig).namespace(namespace); await lastValueFrom( getBackendSrv().fetch({ method: 'DELETE', url: path, showErrorAlert: false, showSuccessAlert: false, params, }) ); }