The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/alerting/unified/utils/misc.ts

319 lines
11 KiB

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<string, string> {
return items.reduce<Record<string, string>>((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<string, string>): Array<{ key: string; value: string }> {
return Object.entries(record).map(([key, value]) => ({ key, value }));
}
type URLParamsLike = ConstructorParameters<typeof URLSearchParams>[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<T, E = Error>(
fn: () => Promise<T>,
shouldRetry: (e: E) => boolean,
timeout: number, // milliseconds, how long to keep retrying
pause = 1000 // milliseconds, pause between retries
): Promise<T> {
const start = new Date().getTime();
const makeAttempt = (): Promise<T> =>
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;
}