import { sortBy } from 'lodash'; import { Labels, UrlQueryMap } from '@grafana/data'; import { GrafanaEdition } from '@grafana/data/src/types/config'; import { config, isFetchError } from '@grafana/runtime'; import { DataSourceRef } from '@grafana/schema'; import { contextSrv } from 'app/core/services/context_srv'; import { escapePathSeparators } from 'app/features/alerting/unified/utils/rule-id'; import { alertInstanceKey, isCloudRuleIdentifier, isGrafanaRuleIdentifier, isPrometheusRuleIdentifier, } from 'app/features/alerting/unified/utils/rules'; import { SortOrder } from 'app/plugins/panel/alertlist/types'; import { Alert, CombinedRule, DataSourceRuleGroupIdentifier, FilterState, RuleIdentifier, RulesSource, SilenceFilterState, } from 'app/types/unified-alerting'; import { GrafanaAlertState, PromAlertingRuleState, PromRuleDTO, mapStateWithReasonToBaseState, } from 'app/types/unified-alerting-dto'; import { ALERTMANAGER_NAME_QUERY_KEY } from './constants'; import { getRulesSourceName } from './datasource'; import { SupportedErrors, getErrorMessageFromCode, isApiMachineryError } from './k8s/errors'; import { getMatcherQueryParams } from './matchers'; import * as ruleId from './rule-id'; import { createAbsoluteUrl, createRelativeUrl } from './url'; export function createViewLink(ruleSource: RulesSource, rule: CombinedRule, returnTo?: string): string { const sourceName = getRulesSourceName(ruleSource); const identifier = ruleId.fromCombinedRule(sourceName, rule); const paramId = encodeURIComponent(ruleId.stringifyIdentifier(identifier)); const paramSource = encodeURIComponent(sourceName); return createRelativeUrl(`/alerting/${paramSource}/${paramId}/view`, returnTo ? { returnTo } : {}); } export function createViewLinkV2( groupIdentifier: DataSourceRuleGroupIdentifier, rule: PromRuleDTO, returnTo?: string ): string { const ruleSourceName = groupIdentifier.rulesSource.name; const identifier = ruleId.fromRule(ruleSourceName, groupIdentifier.namespace.name, groupIdentifier.groupName, rule); const paramId = encodeURIComponent(ruleId.stringifyIdentifier(identifier)); const paramSource = encodeURIComponent(ruleSourceName); return createRelativeUrl(`/alerting/${paramSource}/${paramId}/view`, returnTo ? { returnTo } : {}); } export function createExploreLink(datasource: DataSourceRef, query: string) { const { uid, type } = datasource; return createRelativeUrl(`/explore`, { left: JSON.stringify({ datasource: datasource.uid, queries: [{ refId: 'A', datasource: { uid, type }, expr: query }], range: { from: 'now-1h', to: 'now' }, }), }); } export function createContactPointLink(contactPoint: string, alertManagerSourceName = ''): string { return createRelativeUrl(`/alerting/notifications/receivers/${encodeURIComponent(contactPoint)}/edit`, { alertmanager: alertManagerSourceName, }); } /** * @deprecated Avoid using this - we should instead endeavour to use IDs to link directly to contact points. * This isn't always possible, so only use this if we only have access to a contact point's name */ export function createContactPointSearchLink(contactPoint: string, alertManagerSourceName = ''): string { return createRelativeUrl(`/alerting/notifications`, { search: contactPoint, tab: 'contact_points', alertmanager: alertManagerSourceName, }); } export function createMuteTimingLink(muteTimingName: string, alertManagerSourceName = ''): string { return createRelativeUrl('/alerting/routes/mute-timing/edit', { muteName: muteTimingName, alertmanager: alertManagerSourceName, }); } export function createShareLink(ruleIdentifier: RuleIdentifier): string | undefined { if (isCloudRuleIdentifier(ruleIdentifier) || isPrometheusRuleIdentifier(ruleIdentifier)) { return createAbsoluteUrl( `/alerting/${encodeURIComponent(ruleIdentifier.ruleSourceName)}/${encodeURIComponent(escapePathSeparators(ruleIdentifier.ruleName))}/find` ); } else if (isGrafanaRuleIdentifier(ruleIdentifier)) { return createAbsoluteUrl(`/alerting/grafana/${ruleIdentifier.uid}/view`); } return; } export function arrayToRecord(items: Array<{ key: string; value: string }>): Record { return items.reduce>((rec, { key, value }) => { rec[key] = value; return rec; }, {}); } export const getFiltersFromUrlParams = (queryParams: UrlQueryMap): FilterState => { const queryString = queryParams.queryString === undefined ? undefined : String(queryParams.queryString); const alertState = queryParams.alertState === undefined ? undefined : String(queryParams.alertState); const dataSource = queryParams.dataSource === undefined ? undefined : String(queryParams.dataSource); const ruleType = queryParams.ruleType === undefined ? undefined : String(queryParams.ruleType); const groupBy = queryParams.groupBy === undefined ? undefined : String(queryParams.groupBy).split(','); return { queryString, alertState, dataSource, groupBy, ruleType }; }; export const getNotificationPoliciesFilters = (searchParams: URLSearchParams) => { return { queryString: searchParams.get('queryString') ?? undefined, contactPoint: searchParams.get('contactPoint') ?? undefined, }; }; export const getSilenceFiltersFromUrlParams = (queryParams: UrlQueryMap): SilenceFilterState => { const queryString = queryParams.queryString === undefined ? undefined : String(queryParams.queryString); const silenceState = queryParams.silenceState === undefined ? undefined : String(queryParams.silenceState); return { queryString, silenceState }; }; export function recordToArray(record: Record): Array<{ key: string; value: string }> { return Object.entries(record).map(([key, value]) => ({ key, value })); } type URLParamsLike = ConstructorParameters[0]; export function makeAMLink(path: string, alertManagerName?: string, options?: URLParamsLike): string { const search = new URLSearchParams(options); if (alertManagerName) { search.set(ALERTMANAGER_NAME_QUERY_KEY, alertManagerName); } return `${path}?${search.toString()}`; } export function makeLabelBasedSilenceLink(alertManagerSourceName: string, labels: Labels) { const silenceUrlParams = new URLSearchParams(); silenceUrlParams.append('alertmanager', alertManagerSourceName); const matcherParams = getMatcherQueryParams(labels); matcherParams.forEach((value, key) => silenceUrlParams.append(key, value)); return createRelativeUrl('/alerting/silence/new', silenceUrlParams); } export function makeDataSourceLink(uid: string) { return createRelativeUrl(`/datasources/edit/${uid}`); } export function makeFolderLink(folderUID: string): string { return createRelativeUrl(`/dashboards/f/${folderUID}`); } export function makeFolderAlertsLink(folderUID: string, title: string): string { return createRelativeUrl(`/dashboards/f/${folderUID}/${title}/alerting`); } export function makeFolderSettingsLink(uid: string): string { return createRelativeUrl(`/dashboards/f/${uid}/settings`); } export function makeDashboardLink(dashboardUID: string): string { return createRelativeUrl(`/d/${encodeURIComponent(dashboardUID)}`); } export function makePanelLink(dashboardUID: string, panelId: string): string { const panelParams = new URLSearchParams({ viewPanel: panelId }); return createRelativeUrl(`/d/${encodeURIComponent(dashboardUID)}`, panelParams); } // keep retrying fn if it's error passes shouldRetry(error) and timeout has not elapsed yet export function retryWhile( fn: () => Promise, shouldRetry: (e: E) => boolean, timeout: number, // milliseconds, how long to keep retrying pause = 1000 // milliseconds, pause between retries ): Promise { const start = new Date().getTime(); const makeAttempt = (): Promise => fn().catch((e) => { if (shouldRetry(e) && new Date().getTime() - start < timeout) { return new Promise((resolve) => setTimeout(resolve, pause)).then(makeAttempt); } throw e; }); return makeAttempt(); } const alertStateSortScore = { [GrafanaAlertState.Alerting]: 1, [PromAlertingRuleState.Firing]: 1, [GrafanaAlertState.Error]: 1, [GrafanaAlertState.Pending]: 2, [PromAlertingRuleState.Pending]: 2, [PromAlertingRuleState.Inactive]: 2, [GrafanaAlertState.NoData]: 3, [GrafanaAlertState.Normal]: 4, }; export function sortAlerts(sortOrder: SortOrder, alerts: Alert[]): Alert[] { // Make sure to handle tie-breaks because API returns alert instances in random order every time if (sortOrder === SortOrder.Importance) { return sortBy(alerts, (alert) => [ alertStateSortScore[mapStateWithReasonToBaseState(alert.state)], alertInstanceKey(alert).toLocaleLowerCase(), ]); } else if (sortOrder === SortOrder.TimeAsc) { return sortBy(alerts, (alert) => [ new Date(alert.activeAt) || new Date(), alertInstanceKey(alert).toLocaleLowerCase(), ]); } else if (sortOrder === SortOrder.TimeDesc) { return sortBy(alerts, (alert) => [ new Date(alert.activeAt) || new Date(), alertInstanceKey(alert).toLocaleLowerCase(), ]).reverse(); } const result = sortBy(alerts, (alert) => alertInstanceKey(alert).toLocaleLowerCase()); if (sortOrder === SortOrder.AlphaDesc) { result.reverse(); } return result; } export function isOpenSourceEdition() { const buildInfo = config.buildInfo; return buildInfo.edition === GrafanaEdition.OpenSource; } export function isAdmin() { return contextSrv.hasRole('Admin') || contextSrv.isGrafanaAdmin; } export function isLocalDevEnv() { const buildInfo = config.buildInfo; return buildInfo.env === 'development'; } export function isErrorLike(error: unknown): error is Error { return Boolean(error && typeof error === 'object' && 'message' in error); } export function getErrorCode(error: Error): unknown { if (isApiMachineryError(error) && error.data.details) { return error.data.details.uid; } return error.cause; } /* this function will check if the error passed as the first argument contains an error code */ export function isErrorMatchingCode(error: Error | undefined, code: SupportedErrors): boolean { if (!error) { return false; } return getErrorCode(error) === code; } export function stringifyErrorLike(error: unknown): string { const fetchError = isFetchError(error); if (fetchError) { if (isApiMachineryError(error) && error.data.details) { const message = getErrorMessageFromCode(error.data.details.uid); if (message) { return message; } } if (error.message) { return error.message; } if ('message' in error.data && typeof error.data.message === 'string') { return error.data.message; } if (error.statusText) { return error.statusText; } return String(error.status) || 'Unknown error'; } if (!isErrorLike(error)) { return String(error); } // if the error is one we know how to translate via an error code: const code = getErrorCode(error); if (typeof code === 'string') { const message = getErrorMessageFromCode(code); if (message) { return message; } } if (error.cause) { return `${error.message}, cause: ${stringifyErrorLike(error.cause)}`; } return error.message; }