2025-04-01 10:38:02 +09:00

443 lines
14 KiB
TypeScript

import { capitalize } from 'lodash';
import { AlertState } from '@grafana/data';
import { config } from '@grafana/runtime';
import {
Alert,
AlertingRule,
CloudRuleIdentifier,
CombinedRule,
CombinedRuleGroup,
CombinedRuleWithLocation,
EditableRuleIdentifier,
GrafanaRuleIdentifier,
PromRuleWithLocation,
PrometheusRuleIdentifier,
RecordingRule,
Rule,
RuleGroupIdentifier,
RuleIdentifier,
RuleNamespace,
RuleWithLocation,
RulesSource,
} from 'app/types/unified-alerting';
import {
Annotations,
GrafanaAlertState,
GrafanaAlertStateWithReason,
PostableRuleDTO,
PromAlertingRuleState,
PromRuleType,
RulerAlertingRuleDTO,
RulerCloudRuleDTO,
RulerGrafanaRuleDTO,
RulerRecordingRuleDTO,
RulerRuleDTO,
mapStateWithReasonToBaseState,
} from 'app/types/unified-alerting-dto';
import { CombinedRuleNamespace } from '../../../../types/unified-alerting';
import { State } from '../components/StateTag';
import { RuleHealth } from '../search/rulesSearchParser';
import { RuleFormType, RuleFormValues } from '../types/rule-form';
import { RULER_NOT_SUPPORTED_MSG } from './constants';
import { getRulesSourceName, isGrafanaRulesSource } from './datasource';
import { GRAFANA_ORIGIN_LABEL } from './labels';
import { AsyncRequestState } from './redux';
import { formatPrometheusDuration, safeParsePrometheusDuration } from './time';
export function isAlertingRule(rule: Rule | undefined): rule is AlertingRule {
return typeof rule === 'object' && rule.type === PromRuleType.Alerting;
}
export function isRecordingRule(rule: Rule | undefined): rule is RecordingRule {
return typeof rule === 'object' && rule.type === PromRuleType.Recording;
}
export function isAlertingRulerRule(rule?: RulerRuleDTO): rule is RulerAlertingRuleDTO {
return typeof rule === 'object' && 'alert' in rule;
}
export function isRecordingRulerRule(rule?: RulerRuleDTO): rule is RulerRecordingRuleDTO {
return typeof rule === 'object' && 'record' in rule;
}
export function isGrafanaOrDataSourceRecordingRule(rule?: RulerRuleDTO) {
return (
(typeof rule === 'object' && isRecordingRulerRule(rule)) ||
(isGrafanaRulerRule(rule) && 'record' in rule.grafana_alert)
);
}
export function isGrafanaRecordingRule(rule?: RulerRuleDTO): rule is RulerGrafanaRuleDTO {
return typeof rule === 'object' && isGrafanaOrDataSourceRecordingRule(rule) && isGrafanaRulerRule(rule);
}
export function isGrafanaAlertingRule(rule?: RulerRuleDTO): rule is RulerGrafanaRuleDTO {
return typeof rule === 'object' && isGrafanaRulerRule(rule) && !isGrafanaOrDataSourceRecordingRule(rule);
}
export function isGrafanaRulerRule(rule?: RulerRuleDTO | PostableRuleDTO): rule is RulerGrafanaRuleDTO {
return typeof rule === 'object' && 'grafana_alert' in rule;
}
export function isCloudRulerRule(rule?: RulerRuleDTO | PostableRuleDTO): rule is RulerCloudRuleDTO {
return typeof rule === 'object' && !isGrafanaRulerRule(rule);
}
export function isGrafanaRulerRulePaused(rule: RulerGrafanaRuleDTO) {
return rule && isGrafanaRulerRule(rule) && Boolean(rule.grafana_alert.is_paused);
}
export function alertInstanceKey(alert: Alert): string {
return JSON.stringify(alert.labels);
}
export function isRulerNotSupportedResponse(resp: AsyncRequestState<any>) {
return resp.error && resp.error?.message?.includes(RULER_NOT_SUPPORTED_MSG);
}
export function isGrafanaRuleIdentifier(identifier: RuleIdentifier): identifier is GrafanaRuleIdentifier {
return 'uid' in identifier;
}
export function isCloudRuleIdentifier(identifier: RuleIdentifier): identifier is CloudRuleIdentifier {
return 'rulerRuleHash' in identifier;
}
export function isPromRuleType(ruleType: string): ruleType is PromRuleType {
return Object.values<string>(PromRuleType).includes(ruleType);
}
export function isPrometheusRuleIdentifier(identifier: RuleIdentifier): identifier is PrometheusRuleIdentifier {
return 'ruleHash' in identifier;
}
export function isEditableRuleIdentifier(identifier: RuleIdentifier): identifier is EditableRuleIdentifier {
return isGrafanaRuleIdentifier(identifier) || isCloudRuleIdentifier(identifier);
}
export function isProvisionedRule(rulerRule: RulerRuleDTO): boolean {
return isGrafanaRulerRule(rulerRule) && Boolean(rulerRule.grafana_alert.provenance);
}
export function getRuleHealth(health: string): RuleHealth | undefined {
switch (health) {
case 'ok':
return RuleHealth.Ok;
case 'nodata':
return RuleHealth.NoData;
case 'error':
case 'err': // Prometheus-compat data sources
return RuleHealth.Error;
case 'unknown':
return RuleHealth.Unknown;
default:
return undefined;
}
}
export function getPendingPeriod(rule: CombinedRule): string | undefined {
if (
isRecordingRulerRule(rule.rulerRule) ||
isRecordingRule(rule.promRule) ||
isGrafanaRecordingRule(rule.rulerRule)
) {
return undefined;
}
// We prefer the for duration from the ruler rule because it is formatted as a duration string
// Prometheus duration is in seconds and we need to format it as a duration string
// Additionally, due to eventual consistency of the Prometheus endpoint the ruler data might be newer
if (isAlertingRulerRule(rule.rulerRule)) {
return rule.rulerRule.for;
}
if (isAlertingRule(rule.promRule)) {
const durationInMilliseconds = (rule.promRule.duration ?? 0) * 1000;
return formatPrometheusDuration(durationInMilliseconds);
}
return undefined;
}
export function getAnnotations(rule: CombinedRule): Annotations | undefined {
if (
isRecordingRulerRule(rule.rulerRule) ||
isRecordingRule(rule.promRule) ||
isGrafanaRecordingRule(rule.rulerRule)
) {
return undefined;
}
return rule.annotations ?? [];
}
export interface RulePluginOrigin {
pluginId: string;
}
export function getRulePluginOrigin(rule?: Rule | RulerRuleDTO): RulePluginOrigin | undefined {
if (!rule) {
return undefined;
}
const origin = rule.labels?.[GRAFANA_ORIGIN_LABEL];
if (!origin) {
return undefined;
}
const match = origin.match(/^plugin\/(?<pluginId>.+)$/);
if (!match?.groups) {
return undefined;
}
const pluginId = match.groups.pluginId;
const pluginInstalled = isPluginInstalled(pluginId);
if (!pluginInstalled) {
return undefined;
}
return { pluginId };
}
function isPluginInstalled(pluginId: string) {
return Boolean(config.apps[pluginId]);
}
export function isPluginProvidedRule(rule?: Rule | RulerRuleDTO): boolean {
return Boolean(getRulePluginOrigin(rule));
}
export function alertStateToReadable(state: PromAlertingRuleState | GrafanaAlertStateWithReason | AlertState): string {
if (state === PromAlertingRuleState.Inactive) {
return 'Normal';
}
return capitalize(state);
}
export const flattenRules = (rules: RuleNamespace[]) => {
return rules.reduce<PromRuleWithLocation[]>((acc, { dataSourceName, name: namespaceName, groups }) => {
groups.forEach(({ name: groupName, rules }) => {
rules.forEach((rule) => {
if (isAlertingRule(rule)) {
acc.push({ dataSourceName, namespaceName, groupName, rule });
}
});
});
return acc;
}, []);
};
export const getAlertingRule = (rule: CombinedRuleWithLocation) =>
isAlertingRule(rule.promRule) ? rule.promRule : null;
export const flattenCombinedRules = (rules: CombinedRuleNamespace[]) => {
return rules.reduce<CombinedRuleWithLocation[]>((acc, { rulesSource, name: namespaceName, groups }) => {
groups.forEach(({ name: groupName, rules }) => {
rules.forEach((rule) => {
if (rule.promRule && isAlertingRule(rule.promRule)) {
acc.push({ dataSourceName: getRulesSourceName(rulesSource), namespaceName, groupName, ...rule });
}
});
});
return acc;
}, []);
};
export function alertStateToState(state: PromAlertingRuleState | GrafanaAlertStateWithReason | AlertState): State {
let key: PromAlertingRuleState | GrafanaAlertState | AlertState;
if (Object.values(AlertState).includes(state as AlertState)) {
key = state as AlertState;
} else {
key = mapStateWithReasonToBaseState(state as GrafanaAlertStateWithReason | PromAlertingRuleState);
}
return alertStateToStateMap[key];
}
const alertStateToStateMap: Record<PromAlertingRuleState | GrafanaAlertState | AlertState, State> = {
[PromAlertingRuleState.Inactive]: 'good',
[PromAlertingRuleState.Firing]: 'bad',
[PromAlertingRuleState.Pending]: 'warning',
[GrafanaAlertState.Alerting]: 'bad',
[GrafanaAlertState.Error]: 'bad',
[GrafanaAlertState.NoData]: 'info',
[GrafanaAlertState.Normal]: 'good',
[GrafanaAlertState.Pending]: 'warning',
[AlertState.NoData]: 'info',
[AlertState.Paused]: 'warning',
[AlertState.Alerting]: 'bad',
[AlertState.OK]: 'good',
// AlertState.Pending is not included because the 'pending' value is already covered by `PromAlertingRuleState.Pending`
// [AlertState.Pending]: 'warning',
[AlertState.Unknown]: 'info',
};
export function getFirstActiveAt(promRule?: AlertingRule) {
if (!promRule?.alerts) {
return null;
}
return promRule.alerts.reduce<Date | null>((prev, alert) => {
const isNotNormal = mapStateWithReasonToBaseState(alert.state) !== GrafanaAlertState.Normal;
if (alert.activeAt && isNotNormal) {
const activeAt = new Date(alert.activeAt);
if (prev === null || prev.getTime() > activeAt.getTime()) {
return activeAt;
}
}
return prev;
}, null);
}
/**
* A rule group is "federated" when it has at least one "source_tenants" entry, federated rule groups will evaluate rules in multiple tenants
* Non-federated rules do not have this property
*
* see https://grafana.com/docs/metrics-enterprise/latest/tenant-management/tenant-federation/#cross-tenant-alerting-and-recording-rule-federation
*/
export function isFederatedRuleGroup(group: CombinedRuleGroup) {
return Array.isArray(group.source_tenants);
}
export function getRuleName(rule: RulerRuleDTO) {
if (isGrafanaRulerRule(rule)) {
return rule.grafana_alert.title;
}
if (isAlertingRulerRule(rule)) {
return rule.alert;
}
if (isRecordingRulerRule(rule)) {
return rule.record;
}
return '';
}
export interface AlertInfo {
alertName: string;
forDuration?: string;
evaluationsToFire: number | null;
}
export const getAlertInfo = (alert: RulerRuleDTO, currentEvaluation: string): AlertInfo => {
const emptyAlert: AlertInfo = {
alertName: '',
forDuration: '0s',
evaluationsToFire: 0,
};
if (isGrafanaRulerRule(alert)) {
return {
alertName: alert.grafana_alert.title,
forDuration: alert.for,
evaluationsToFire: alert.for ? getNumberEvaluationsToStartAlerting(alert.for, currentEvaluation) : null,
};
}
if (isAlertingRulerRule(alert)) {
return {
alertName: alert.alert,
forDuration: alert.for ?? '0s',
evaluationsToFire: getNumberEvaluationsToStartAlerting(alert.for ?? '0s', currentEvaluation),
};
}
return emptyAlert;
};
export const getNumberEvaluationsToStartAlerting = (forDuration: string, currentEvaluation: string) => {
const evalNumberMs = safeParsePrometheusDuration(currentEvaluation);
const forNumber = safeParsePrometheusDuration(forDuration);
if (forNumber === 0 && evalNumberMs !== 0) {
return 1;
}
if (evalNumberMs === 0) {
return 0;
} else {
const evaluationsBeforeCeil = forNumber / evalNumberMs;
return evaluationsBeforeCeil < 1 ? 0 : Math.ceil(forNumber / evalNumberMs) + 1;
}
};
/*
* Extracts a rule group identifier from a CombinedRule
*/
export function getRuleGroupLocationFromCombinedRule(rule: CombinedRule): RuleGroupIdentifier {
const ruleSourceName = isGrafanaRulesSource(rule.namespace.rulesSource)
? rule.namespace.rulesSource
: rule.namespace.rulesSource.name;
const namespace = isGrafanaRulerRule(rule.rulerRule)
? rule.rulerRule.grafana_alert.namespace_uid
: rule.namespace.name;
return {
dataSourceName: ruleSourceName,
namespaceName: namespace,
groupName: rule.group.name,
};
}
/**
* Extracts a rule group identifier from a RuleWithLocation
*/
export function getRuleGroupLocationFromRuleWithLocation(rule: RuleWithLocation): RuleGroupIdentifier {
const dataSourceName = rule.ruleSourceName;
const namespaceName = isGrafanaRulerRule(rule.rule) ? rule.rule.grafana_alert.namespace_uid : rule.namespace;
const groupName = rule.group.name;
return {
dataSourceName,
namespaceName,
groupName,
};
}
export function getRuleGroupLocationFromFormValues(values: RuleFormValues): RuleGroupIdentifier {
const dataSourceName = values.dataSourceName;
const namespaceName = values.folder?.uid ?? values.namespace;
const groupName = values.group;
if (!dataSourceName) {
throw new Error('no datasource name in form values');
}
return {
dataSourceName,
namespaceName,
groupName,
};
}
export function rulesSourceToDataSourceName(rulesSource: RulesSource): string {
return isGrafanaRulesSource(rulesSource) ? rulesSource : rulesSource.name;
}
export function isGrafanaAlertingRuleByType(type?: RuleFormType) {
return type === RuleFormType.grafana;
}
export function isGrafanaRecordingRuleByType(type?: RuleFormType) {
return type === RuleFormType.grafanaRecording;
}
export function isCloudAlertingRuleByType(type?: RuleFormType) {
return type === RuleFormType.cloudAlerting;
}
export function isCloudRecordingRuleByType(type?: RuleFormType) {
return type === RuleFormType.cloudRecording;
}
export function isGrafanaManagedRuleByType(type?: RuleFormType) {
return isGrafanaAlertingRuleByType(type) || isGrafanaRecordingRuleByType(type);
}
export function isRecordingRuleByType(type?: RuleFormType) {
return isGrafanaRecordingRuleByType(type) || isCloudRecordingRuleByType(type);
}
export function isDataSourceManagedRuleByType(type?: RuleFormType) {
return isCloudAlertingRuleByType(type) || isCloudRecordingRuleByType(type);
}