mirror of https://github.com/grafana/grafana.git
935 lines
29 KiB
TypeScript
935 lines
29 KiB
TypeScript
import { clamp, omit } from 'lodash';
|
|
|
|
import {
|
|
DataQuery,
|
|
DataSourceInstanceSettings,
|
|
DataSourceRef,
|
|
getDefaultRelativeTimeRange,
|
|
getNextRefId,
|
|
IntervalValues,
|
|
rangeUtil,
|
|
RelativeTimeRange,
|
|
ScopedVars,
|
|
TimeRange,
|
|
} from '@grafana/data';
|
|
import { PromQuery } from '@grafana/prometheus';
|
|
import { config, getDataSourceSrv } from '@grafana/runtime';
|
|
import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend';
|
|
import { sceneGraph, VizPanel } from '@grafana/scenes';
|
|
import { DataSourceJsonData } from '@grafana/schema';
|
|
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
|
import {
|
|
getDashboardSceneFor,
|
|
getPanelIdForVizPanel,
|
|
getQueryRunnerFor,
|
|
} from 'app/features/dashboard-scene/utils/utils';
|
|
import { ExpressionDatasourceUID, ExpressionQuery, ExpressionQueryType } from 'app/features/expressions/types';
|
|
import { LokiQuery } from 'app/plugins/datasource/loki/types';
|
|
import { RuleWithLocation } from 'app/types/unified-alerting';
|
|
import {
|
|
AlertDataQuery,
|
|
AlertQuery,
|
|
Annotations,
|
|
GrafanaAlertStateDecision,
|
|
GrafanaNotificationSettings,
|
|
GrafanaRuleDefinition,
|
|
Labels,
|
|
PostableRuleGrafanaRuleDTO,
|
|
RulerAlertingRuleDTO,
|
|
RulerRecordingRuleDTO,
|
|
RulerRuleDTO,
|
|
} from 'app/types/unified-alerting-dto';
|
|
|
|
type KVObject = { key: string; value: string };
|
|
|
|
import { EvalFunction } from '../../state/alertDef';
|
|
import {
|
|
AlertManagerManualRouting,
|
|
ContactPoint,
|
|
RuleFormType,
|
|
RuleFormValues,
|
|
SimplifiedEditor,
|
|
} from '../types/rule-form';
|
|
|
|
import { getRulesAccess } from './access-control';
|
|
import { Annotation, defaultAnnotations } from './constants';
|
|
import {
|
|
DataSourceType,
|
|
getDefaultOrFirstCompatibleDataSource,
|
|
GRAFANA_RULES_SOURCE_NAME,
|
|
isGrafanaRulesSource,
|
|
} from './datasource';
|
|
import { arrayToRecord, recordToArray } from './misc';
|
|
import {
|
|
isAlertingRulerRule,
|
|
isGrafanaAlertingRuleByType,
|
|
isGrafanaRecordingRule,
|
|
isGrafanaRecordingRuleByType,
|
|
isGrafanaRulerRule,
|
|
isRecordingRulerRule,
|
|
} from './rules';
|
|
import { formatPrometheusDuration, parseInterval, safeParsePrometheusDuration } from './time';
|
|
|
|
export type PromOrLokiQuery = PromQuery | LokiQuery;
|
|
|
|
export const MANUAL_ROUTING_KEY = 'grafana.alerting.manualRouting';
|
|
export const SIMPLIFIED_QUERY_EDITOR_KEY = 'grafana.alerting.simplifiedQueryEditor';
|
|
|
|
// even if the min interval is < 1m we should default to 1m, but allow arbitrary values for minInterval > 1m
|
|
const GROUP_EVALUATION_MIN_INTERVAL_MS = safeParsePrometheusDuration(config.unifiedAlerting?.minInterval ?? '10s');
|
|
const GROUP_EVALUATION_INTERVAL_LOWER_BOUND = safeParsePrometheusDuration('1m');
|
|
const GROUP_EVALUATION_INTERVAL_UPPER_BOUND = Infinity;
|
|
|
|
export const DEFAULT_GROUP_EVALUATION_INTERVAL = formatPrometheusDuration(
|
|
clamp(GROUP_EVALUATION_MIN_INTERVAL_MS, GROUP_EVALUATION_INTERVAL_LOWER_BOUND, GROUP_EVALUATION_INTERVAL_UPPER_BOUND)
|
|
);
|
|
|
|
export const getDefaultFormValues = (): RuleFormValues => {
|
|
const { canCreateGrafanaRules, canCreateCloudRules } = getRulesAccess();
|
|
|
|
return Object.freeze({
|
|
name: '',
|
|
uid: '',
|
|
labels: [{ key: '', value: '' }],
|
|
annotations: defaultAnnotations,
|
|
dataSourceName: GRAFANA_RULES_SOURCE_NAME, // let's use Grafana-managed alert rule by default
|
|
type: canCreateGrafanaRules ? RuleFormType.grafana : canCreateCloudRules ? RuleFormType.cloudAlerting : undefined, // viewers can't create prom alerts
|
|
group: '',
|
|
|
|
// grafana
|
|
folder: undefined,
|
|
queries: [],
|
|
recordingRulesQueries: [],
|
|
condition: '',
|
|
noDataState: GrafanaAlertStateDecision.NoData,
|
|
execErrState: GrafanaAlertStateDecision.Error,
|
|
evaluateFor: DEFAULT_GROUP_EVALUATION_INTERVAL,
|
|
evaluateEvery: DEFAULT_GROUP_EVALUATION_INTERVAL,
|
|
manualRouting: getDefautManualRouting(), // we default to true if the feature toggle is enabled and the user hasn't set local storage to false
|
|
contactPoints: {},
|
|
overrideGrouping: false,
|
|
overrideTimings: false,
|
|
muteTimeIntervals: [],
|
|
editorSettings: getDefaultEditorSettings(),
|
|
|
|
// cortex / loki
|
|
namespace: '',
|
|
expression: '',
|
|
forTime: 1,
|
|
forTimeUnit: 'm',
|
|
});
|
|
};
|
|
|
|
export const getDefautManualRouting = () => {
|
|
// first check if feature toggle for simplified routing is enabled
|
|
const simplifiedRoutingToggleEnabled = config.featureToggles.alertingSimplifiedRouting ?? false;
|
|
if (!simplifiedRoutingToggleEnabled) {
|
|
return false;
|
|
}
|
|
//then, check in local storage if the user has enabled simplified routing
|
|
// if it's not set, we'll default to true
|
|
const manualRouting = localStorage.getItem(MANUAL_ROUTING_KEY);
|
|
return manualRouting !== 'false';
|
|
};
|
|
|
|
function getDefaultEditorSettings() {
|
|
const editorSettingsEnabled = config.featureToggles.alertingQueryAndExpressionsStepMode ?? false;
|
|
if (!editorSettingsEnabled) {
|
|
return undefined;
|
|
}
|
|
//then, check in local storage if the user has saved last rule with simplified query editor
|
|
const queryEditorSettings = localStorage.getItem(SIMPLIFIED_QUERY_EDITOR_KEY);
|
|
return {
|
|
simplifiedQueryEditor: queryEditorSettings !== 'false',
|
|
};
|
|
}
|
|
|
|
export function formValuesToRulerRuleDTO(values: RuleFormValues): RulerRuleDTO {
|
|
const { name, expression, forTime, forTimeUnit, keepFiringForTime, keepFiringForTimeUnit, type } = values;
|
|
|
|
const annotations = arrayToRecord(cleanAnnotations(values.annotations));
|
|
const labels = arrayToRecord(cleanLabels(values.labels));
|
|
|
|
if (type === RuleFormType.cloudAlerting) {
|
|
let keepFiringFor: string | undefined;
|
|
if (keepFiringForTime && keepFiringForTimeUnit) {
|
|
keepFiringFor = `${keepFiringForTime}${keepFiringForTimeUnit}`;
|
|
}
|
|
|
|
return {
|
|
alert: name,
|
|
for: `${forTime}${forTimeUnit}`,
|
|
keep_firing_for: keepFiringFor,
|
|
annotations,
|
|
labels,
|
|
expr: expression,
|
|
};
|
|
} else if (type === RuleFormType.cloudRecording) {
|
|
return {
|
|
record: name,
|
|
labels,
|
|
expr: expression,
|
|
};
|
|
}
|
|
throw new Error(`unexpected rule type: ${type}`);
|
|
}
|
|
|
|
export function listifyLabelsOrAnnotations(item: Labels | Annotations | undefined, addEmpty: boolean): KVObject[] {
|
|
const list = [...recordToArray(item || {})];
|
|
if (addEmpty) {
|
|
list.push({ key: '', value: '' });
|
|
}
|
|
return list;
|
|
}
|
|
|
|
//make sure default annotations are always shown in order even if empty
|
|
export function normalizeDefaultAnnotations(annotations: KVObject[]) {
|
|
const orderedAnnotations = [...annotations];
|
|
const defaultAnnotationKeys = defaultAnnotations.map((annotation) => annotation.key);
|
|
|
|
defaultAnnotationKeys.forEach((defaultAnnotationKey, index) => {
|
|
const fieldIndex = orderedAnnotations.findIndex((field) => field.key === defaultAnnotationKey);
|
|
|
|
if (fieldIndex === -1) {
|
|
//add the default annotation if abstent
|
|
const emptyValue = { key: defaultAnnotationKey, value: '' };
|
|
orderedAnnotations.splice(index, 0, emptyValue);
|
|
} else if (fieldIndex !== index) {
|
|
//move it to the correct position if present
|
|
orderedAnnotations.splice(index, 0, orderedAnnotations.splice(fieldIndex, 1)[0]);
|
|
}
|
|
});
|
|
return orderedAnnotations;
|
|
}
|
|
|
|
export function getNotificationSettingsForDTO(
|
|
manualRouting: boolean,
|
|
contactPoints?: AlertManagerManualRouting
|
|
): GrafanaNotificationSettings | undefined {
|
|
if (contactPoints?.grafana?.selectedContactPoint && manualRouting) {
|
|
return {
|
|
receiver: contactPoints?.grafana?.selectedContactPoint,
|
|
mute_time_intervals: contactPoints?.grafana?.muteTimeIntervals,
|
|
group_by: contactPoints?.grafana?.overrideGrouping ? contactPoints?.grafana?.groupBy : undefined,
|
|
group_wait:
|
|
contactPoints?.grafana?.overrideTimings && contactPoints?.grafana?.groupWaitValue
|
|
? contactPoints?.grafana?.groupWaitValue
|
|
: undefined,
|
|
group_interval:
|
|
contactPoints?.grafana?.overrideTimings && contactPoints?.grafana?.groupIntervalValue
|
|
? contactPoints?.grafana?.groupIntervalValue
|
|
: undefined,
|
|
repeat_interval:
|
|
contactPoints?.grafana?.overrideTimings && contactPoints?.grafana?.repeatIntervalValue
|
|
? contactPoints?.grafana?.repeatIntervalValue
|
|
: undefined,
|
|
};
|
|
}
|
|
return undefined;
|
|
}
|
|
function getEditorSettingsForDTO(simplifiedEditor: SimplifiedEditor) {
|
|
return {
|
|
simplified_query_and_expressions_section: simplifiedEditor.simplifiedQueryEditor,
|
|
};
|
|
}
|
|
export function formValuesToRulerGrafanaRuleDTO(values: RuleFormValues): PostableRuleGrafanaRuleDTO {
|
|
const {
|
|
name,
|
|
condition,
|
|
noDataState,
|
|
execErrState,
|
|
evaluateFor,
|
|
queries,
|
|
isPaused,
|
|
contactPoints,
|
|
manualRouting,
|
|
type,
|
|
metric,
|
|
} = values;
|
|
if (!condition) {
|
|
throw new Error('You cannot create an alert rule without specifying the alert condition');
|
|
}
|
|
|
|
const notificationSettings = getNotificationSettingsForDTO(manualRouting, contactPoints);
|
|
const metadata = values.editorSettings
|
|
? { editor_settings: getEditorSettingsForDTO(values.editorSettings) }
|
|
: undefined;
|
|
|
|
const annotations = arrayToRecord(cleanAnnotations(values.annotations));
|
|
const labels = arrayToRecord(cleanLabels(values.labels));
|
|
|
|
const wantsAlertingRule = isGrafanaAlertingRuleByType(type);
|
|
const wantsRecordingRule = isGrafanaRecordingRuleByType(type!);
|
|
|
|
if (wantsAlertingRule) {
|
|
return {
|
|
grafana_alert: {
|
|
title: name,
|
|
condition,
|
|
data: queries.map(fixBothInstantAndRangeQuery),
|
|
is_paused: Boolean(isPaused),
|
|
|
|
// Alerting rule specific
|
|
no_data_state: noDataState,
|
|
exec_err_state: execErrState,
|
|
notification_settings: notificationSettings,
|
|
metadata,
|
|
},
|
|
annotations,
|
|
labels,
|
|
|
|
// Alerting rule specific
|
|
for: evaluateFor,
|
|
};
|
|
} else if (wantsRecordingRule) {
|
|
return {
|
|
grafana_alert: {
|
|
title: name,
|
|
condition,
|
|
data: queries.map(fixBothInstantAndRangeQuery),
|
|
is_paused: Boolean(isPaused),
|
|
|
|
// Recording rule specific
|
|
record: {
|
|
metric: metric ?? name,
|
|
from: condition,
|
|
},
|
|
},
|
|
annotations,
|
|
labels,
|
|
};
|
|
}
|
|
|
|
throw new Error(`Failed to convert form values to Grafana rule: unknown type ${type}`);
|
|
}
|
|
|
|
export const cleanAnnotations = (kvs: KVObject[]) =>
|
|
kvs.map(trimKeyAndValue).filter(({ key, value }: KVObject): Boolean => Boolean(key) && Boolean(value));
|
|
|
|
export const cleanLabels = (kvs: KVObject[]) =>
|
|
kvs.map(trimKeyAndValue).filter(({ key }: KVObject): Boolean => Boolean(key));
|
|
|
|
const trimKeyAndValue = ({ key, value }: KVObject): KVObject => ({
|
|
key: key.trim(),
|
|
value: value.trim(),
|
|
});
|
|
|
|
export function getContactPointsFromDTO(ga: GrafanaRuleDefinition): AlertManagerManualRouting | undefined {
|
|
const contactPoint: ContactPoint | undefined = ga.notification_settings
|
|
? {
|
|
selectedContactPoint: ga.notification_settings.receiver,
|
|
muteTimeIntervals: ga.notification_settings.mute_time_intervals ?? [],
|
|
overrideGrouping:
|
|
Array.isArray(ga.notification_settings.group_by) && ga.notification_settings.group_by.length > 0,
|
|
overrideTimings: [
|
|
ga.notification_settings.group_wait,
|
|
ga.notification_settings.group_interval,
|
|
ga.notification_settings.repeat_interval,
|
|
].some(Boolean),
|
|
groupBy: ga.notification_settings.group_by || [],
|
|
groupWaitValue: ga.notification_settings.group_wait || '',
|
|
groupIntervalValue: ga.notification_settings.group_interval || '',
|
|
repeatIntervalValue: ga.notification_settings.repeat_interval || '',
|
|
}
|
|
: undefined;
|
|
const routingSettings: AlertManagerManualRouting | undefined = contactPoint
|
|
? {
|
|
[GRAFANA_RULES_SOURCE_NAME]: contactPoint,
|
|
}
|
|
: undefined;
|
|
return routingSettings;
|
|
}
|
|
|
|
function getEditorSettingsFromDTO(ga: GrafanaRuleDefinition) {
|
|
// we need to check if the feature toggle is enabled as it might be disabled after the rule was created with the feature enabled
|
|
if (!config.featureToggles.alertingQueryAndExpressionsStepMode) {
|
|
return undefined;
|
|
}
|
|
|
|
if (ga.metadata?.editor_settings) {
|
|
return {
|
|
simplifiedQueryEditor: ga.metadata.editor_settings.simplified_query_and_expressions_section,
|
|
};
|
|
}
|
|
|
|
return {
|
|
simplifiedQueryEditor: false,
|
|
};
|
|
}
|
|
|
|
export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleFormValues {
|
|
const { ruleSourceName, namespace, group, rule } = ruleWithLocation;
|
|
|
|
const defaultFormValues = getDefaultFormValues();
|
|
if (isGrafanaRulesSource(ruleSourceName)) {
|
|
// GRAFANA-MANAGED RULES
|
|
if (isGrafanaRecordingRule(rule)) {
|
|
// grafana recording rule
|
|
const ga = rule.grafana_alert;
|
|
return {
|
|
...defaultFormValues,
|
|
name: ga.title,
|
|
type: RuleFormType.grafanaRecording,
|
|
group: group.name,
|
|
evaluateEvery: group.interval || defaultFormValues.evaluateEvery,
|
|
queries: ga.data,
|
|
condition: ga.condition,
|
|
annotations: normalizeDefaultAnnotations(listifyLabelsOrAnnotations(rule.annotations, false)),
|
|
labels: listifyLabelsOrAnnotations(rule.labels, true),
|
|
folder: { title: namespace, uid: ga.namespace_uid },
|
|
isPaused: ga.is_paused,
|
|
metric: ga.record?.metric,
|
|
};
|
|
} else if (isGrafanaRulerRule(rule)) {
|
|
// grafana alerting rule
|
|
const ga = rule.grafana_alert;
|
|
const routingSettings: AlertManagerManualRouting | undefined = getContactPointsFromDTO(ga);
|
|
if (ga.no_data_state !== undefined && ga.exec_err_state !== undefined) {
|
|
return {
|
|
...defaultFormValues,
|
|
name: ga.title,
|
|
type: RuleFormType.grafana,
|
|
group: group.name,
|
|
evaluateEvery: group.interval || defaultFormValues.evaluateEvery,
|
|
evaluateFor: rule.for || '0',
|
|
noDataState: ga.no_data_state,
|
|
execErrState: ga.exec_err_state,
|
|
queries: ga.data,
|
|
condition: ga.condition,
|
|
annotations: normalizeDefaultAnnotations(listifyLabelsOrAnnotations(rule.annotations, false)),
|
|
labels: listifyLabelsOrAnnotations(rule.labels, true),
|
|
folder: { title: namespace, uid: ga.namespace_uid },
|
|
isPaused: ga.is_paused,
|
|
|
|
contactPoints: routingSettings,
|
|
manualRouting: Boolean(routingSettings),
|
|
|
|
editorSettings: getEditorSettingsFromDTO(ga),
|
|
};
|
|
} else {
|
|
throw new Error('Unexpected type of rule for grafana rules source');
|
|
}
|
|
} else {
|
|
throw new Error('Unexpected type of rule for grafana rules source');
|
|
}
|
|
} else {
|
|
// DATASOURCE-MANAGED RULES
|
|
if (isAlertingRulerRule(rule)) {
|
|
const datasourceUid = getDataSourceSrv().getInstanceSettings(ruleSourceName)?.uid ?? '';
|
|
|
|
const defaultQuery = {
|
|
refId: 'A',
|
|
datasourceUid,
|
|
queryType: '',
|
|
relativeTimeRange: getDefaultRelativeTimeRange(),
|
|
expr: rule.expr,
|
|
model: {
|
|
refId: 'A',
|
|
hide: false,
|
|
expr: rule.expr,
|
|
},
|
|
};
|
|
|
|
const alertingRuleValues = alertingRulerRuleToRuleForm(rule);
|
|
|
|
return {
|
|
...defaultFormValues,
|
|
...alertingRuleValues,
|
|
queries: [defaultQuery],
|
|
annotations: normalizeDefaultAnnotations(listifyLabelsOrAnnotations(rule.annotations, false)),
|
|
type: RuleFormType.cloudAlerting,
|
|
dataSourceName: ruleSourceName,
|
|
namespace,
|
|
group: group.name,
|
|
};
|
|
} else if (isRecordingRulerRule(rule)) {
|
|
const recordingRuleValues = recordingRulerRuleToRuleForm(rule);
|
|
|
|
return {
|
|
...defaultFormValues,
|
|
...recordingRuleValues,
|
|
type: RuleFormType.cloudRecording,
|
|
dataSourceName: ruleSourceName,
|
|
namespace,
|
|
group: group.name,
|
|
};
|
|
} else {
|
|
throw new Error('Unexpected type of rule for cloud rules source');
|
|
}
|
|
}
|
|
}
|
|
|
|
export function alertingRulerRuleToRuleForm(
|
|
rule: RulerAlertingRuleDTO
|
|
): Pick<
|
|
RuleFormValues,
|
|
| 'name'
|
|
| 'forTime'
|
|
| 'forTimeUnit'
|
|
| 'keepFiringForTime'
|
|
| 'keepFiringForTimeUnit'
|
|
| 'expression'
|
|
| 'annotations'
|
|
| 'labels'
|
|
> {
|
|
const defaultFormValues = getDefaultFormValues();
|
|
|
|
const [forTime, forTimeUnit] = rule.for ? parseInterval(rule.for) : [0, 's'];
|
|
|
|
const [keepFiringForTime, keepFiringForTimeUnit] = rule.keep_firing_for
|
|
? parseInterval(rule.keep_firing_for)
|
|
: [defaultFormValues.keepFiringForTime, defaultFormValues.keepFiringForTimeUnit];
|
|
|
|
return {
|
|
name: rule.alert,
|
|
expression: rule.expr,
|
|
forTime,
|
|
forTimeUnit,
|
|
keepFiringForTime,
|
|
keepFiringForTimeUnit,
|
|
annotations: listifyLabelsOrAnnotations(rule.annotations, false),
|
|
labels: listifyLabelsOrAnnotations(rule.labels, true),
|
|
};
|
|
}
|
|
|
|
export function recordingRulerRuleToRuleForm(
|
|
rule: RulerRecordingRuleDTO
|
|
): Pick<RuleFormValues, 'name' | 'expression' | 'labels'> {
|
|
return {
|
|
name: rule.record,
|
|
expression: rule.expr,
|
|
labels: listifyLabelsOrAnnotations(rule.labels, true),
|
|
};
|
|
}
|
|
|
|
export const getDefaultQueries = (isRecordingRule = false): AlertQuery[] => {
|
|
const dataSource = getDefaultOrFirstCompatibleDataSource();
|
|
if (!dataSource) {
|
|
const expressions = isRecordingRule ? getDefaultExpressionsForRecording('A') : getDefaultExpressions('A', 'B');
|
|
return [...expressions];
|
|
}
|
|
const relativeTimeRange = getDefaultRelativeTimeRange();
|
|
|
|
const expressions = isRecordingRule ? getDefaultExpressionsForRecording('B') : getDefaultExpressions('B', 'C');
|
|
const isLokiOrPrometheus = dataSource?.type === DataSourceType.Prometheus || dataSource?.type === DataSourceType.Loki;
|
|
return [
|
|
{
|
|
refId: 'A',
|
|
datasourceUid: dataSource.uid,
|
|
queryType: '',
|
|
relativeTimeRange,
|
|
model: {
|
|
refId: 'A',
|
|
instant: isLokiOrPrometheus ? true : undefined,
|
|
},
|
|
},
|
|
...expressions,
|
|
];
|
|
};
|
|
|
|
export const getDefaultRecordingRulesQueries = (
|
|
rulesSourcesWithRuler: Array<DataSourceInstanceSettings<DataSourceJsonData>>
|
|
): AlertQuery[] => {
|
|
const relativeTimeRange = getDefaultRelativeTimeRange();
|
|
|
|
return [
|
|
{
|
|
refId: 'A',
|
|
datasourceUid: rulesSourcesWithRuler[0]?.uid || '',
|
|
queryType: '',
|
|
relativeTimeRange,
|
|
model: {
|
|
refId: 'A',
|
|
},
|
|
},
|
|
];
|
|
};
|
|
const getDefaultExpressions = (...refIds: [string, string]): AlertQuery[] => {
|
|
const refOne = refIds[0];
|
|
const refTwo = refIds[1];
|
|
|
|
const reduceExpression: ExpressionQuery = {
|
|
refId: refIds[0],
|
|
type: ExpressionQueryType.reduce,
|
|
datasource: {
|
|
uid: ExpressionDatasourceUID,
|
|
type: ExpressionDatasourceRef.type,
|
|
},
|
|
conditions: [
|
|
{
|
|
type: 'query',
|
|
evaluator: {
|
|
params: [],
|
|
type: EvalFunction.IsAbove,
|
|
},
|
|
operator: {
|
|
type: 'and',
|
|
},
|
|
query: {
|
|
params: [refOne],
|
|
},
|
|
reducer: {
|
|
params: [],
|
|
type: 'last',
|
|
},
|
|
},
|
|
],
|
|
reducer: 'last',
|
|
expression: 'A',
|
|
};
|
|
|
|
const thresholdExpression: ExpressionQuery = {
|
|
refId: refTwo,
|
|
type: ExpressionQueryType.threshold,
|
|
datasource: {
|
|
uid: ExpressionDatasourceUID,
|
|
type: ExpressionDatasourceRef.type,
|
|
},
|
|
conditions: [
|
|
{
|
|
type: 'query',
|
|
evaluator: {
|
|
params: [0],
|
|
type: EvalFunction.IsAbove,
|
|
},
|
|
operator: {
|
|
type: 'and',
|
|
},
|
|
query: {
|
|
params: [refTwo],
|
|
},
|
|
reducer: {
|
|
params: [],
|
|
type: 'last',
|
|
},
|
|
},
|
|
],
|
|
expression: refOne,
|
|
};
|
|
|
|
return [
|
|
{
|
|
refId: refOne,
|
|
datasourceUid: ExpressionDatasourceUID,
|
|
queryType: '',
|
|
model: reduceExpression,
|
|
},
|
|
{
|
|
refId: refTwo,
|
|
datasourceUid: ExpressionDatasourceUID,
|
|
queryType: '',
|
|
model: thresholdExpression,
|
|
},
|
|
];
|
|
};
|
|
const getDefaultExpressionsForRecording = (refOne: string): AlertQuery[] => {
|
|
const reduceExpression: ExpressionQuery = {
|
|
refId: refOne,
|
|
type: ExpressionQueryType.reduce,
|
|
datasource: {
|
|
uid: ExpressionDatasourceUID,
|
|
type: ExpressionDatasourceRef.type,
|
|
},
|
|
conditions: [
|
|
{
|
|
type: 'query',
|
|
evaluator: {
|
|
params: [],
|
|
type: EvalFunction.IsAbove,
|
|
},
|
|
operator: {
|
|
type: 'and',
|
|
},
|
|
query: {
|
|
params: [refOne],
|
|
},
|
|
reducer: {
|
|
params: [],
|
|
type: 'last',
|
|
},
|
|
},
|
|
],
|
|
reducer: 'last',
|
|
expression: 'A',
|
|
};
|
|
|
|
return [
|
|
{
|
|
refId: refOne,
|
|
datasourceUid: ExpressionDatasourceUID,
|
|
queryType: '',
|
|
model: reduceExpression,
|
|
},
|
|
];
|
|
};
|
|
|
|
const dataQueriesToGrafanaQueries = async (
|
|
queries: DataQuery[],
|
|
relativeTimeRange: RelativeTimeRange,
|
|
scopedVars: ScopedVars | {},
|
|
panelDataSourceRef?: DataSourceRef,
|
|
maxDataPoints?: number,
|
|
minInterval?: string
|
|
): Promise<AlertQuery[]> => {
|
|
const result: AlertQuery[] = [];
|
|
|
|
for (const target of queries) {
|
|
const datasource = await getDataSourceSrv().get(target.datasource?.uid ? target.datasource : panelDataSourceRef);
|
|
const dsRef = { uid: datasource.uid, type: datasource.type };
|
|
|
|
const range = rangeUtil.relativeToTimeRange(relativeTimeRange);
|
|
const { interval, intervalMs } = getIntervals(range, minInterval ?? datasource.interval, maxDataPoints);
|
|
const queryVariables = {
|
|
__interval: { text: interval, value: interval },
|
|
__interval_ms: { text: intervalMs, value: intervalMs },
|
|
...scopedVars,
|
|
};
|
|
|
|
const interpolatedTarget = datasource.interpolateVariablesInQueries
|
|
? datasource.interpolateVariablesInQueries([target], queryVariables)[0]
|
|
: target;
|
|
|
|
// expressions
|
|
if (dsRef.uid === ExpressionDatasourceUID) {
|
|
const newQuery: AlertQuery = {
|
|
refId: interpolatedTarget.refId,
|
|
queryType: '',
|
|
relativeTimeRange,
|
|
datasourceUid: ExpressionDatasourceUID,
|
|
model: interpolatedTarget,
|
|
};
|
|
result.push(newQuery);
|
|
// queries
|
|
} else {
|
|
const datasourceSettings = getDataSourceSrv().getInstanceSettings(dsRef);
|
|
if (datasourceSettings && datasourceSettings.meta.alerting) {
|
|
const newQuery: AlertQuery = {
|
|
refId: interpolatedTarget.refId,
|
|
queryType: interpolatedTarget.queryType ?? '',
|
|
relativeTimeRange,
|
|
datasourceUid: datasourceSettings.uid,
|
|
model: {
|
|
...interpolatedTarget,
|
|
maxDataPoints,
|
|
intervalMs,
|
|
},
|
|
};
|
|
result.push(newQuery);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
|
|
export const panelToRuleFormValues = async (
|
|
panel: PanelModel,
|
|
dashboard: DashboardModel
|
|
): Promise<Partial<RuleFormValues> | undefined> => {
|
|
const { targets } = panel;
|
|
if (!panel.id || !dashboard.uid) {
|
|
return undefined;
|
|
}
|
|
|
|
const relativeTimeRange = rangeUtil.timeRangeToRelative(rangeUtil.convertRawToRange(dashboard.time));
|
|
const queries = await dataQueriesToGrafanaQueries(
|
|
targets,
|
|
relativeTimeRange,
|
|
panel.scopedVars || {},
|
|
panel.datasource ?? undefined,
|
|
panel.maxDataPoints ?? undefined,
|
|
panel.interval ?? undefined
|
|
);
|
|
// if no alerting capable queries are found, can't create a rule
|
|
if (!queries.length || !queries.find((query) => query.datasourceUid !== ExpressionDatasourceUID)) {
|
|
return undefined;
|
|
}
|
|
|
|
if (!queries.find((query) => query.datasourceUid === ExpressionDatasourceUID)) {
|
|
const [reduceExpression, _thresholdExpression] = getDefaultExpressions(getNextRefId(queries), '-');
|
|
queries.push(reduceExpression);
|
|
|
|
const [_reduceExpression, thresholdExpression] = getDefaultExpressions(
|
|
reduceExpression.refId,
|
|
getNextRefId(queries)
|
|
);
|
|
queries.push(thresholdExpression);
|
|
}
|
|
|
|
const { folderTitle, folderUid } = dashboard.meta;
|
|
const folder =
|
|
folderUid && folderTitle
|
|
? {
|
|
kind: 'folder',
|
|
uid: folderUid,
|
|
title: folderTitle,
|
|
}
|
|
: undefined;
|
|
|
|
const formValues = {
|
|
type: RuleFormType.grafana,
|
|
folder,
|
|
queries,
|
|
name: panel.title,
|
|
condition: queries[queries.length - 1].refId,
|
|
annotations: [
|
|
{
|
|
key: Annotation.dashboardUID,
|
|
value: dashboard.uid,
|
|
},
|
|
{
|
|
key: Annotation.panelID,
|
|
value: String(panel.id),
|
|
},
|
|
],
|
|
};
|
|
return formValues;
|
|
};
|
|
|
|
export const scenesPanelToRuleFormValues = async (vizPanel: VizPanel): Promise<Partial<RuleFormValues> | undefined> => {
|
|
if (!vizPanel.state.key) {
|
|
return undefined;
|
|
}
|
|
|
|
const timeRange = sceneGraph.getTimeRange(vizPanel);
|
|
const queryRunner = getQueryRunnerFor(vizPanel);
|
|
if (!queryRunner) {
|
|
return undefined;
|
|
}
|
|
const { queries, datasource, maxDataPoints, minInterval } = queryRunner.state;
|
|
|
|
const dashboard = getDashboardSceneFor(vizPanel);
|
|
if (!dashboard || !dashboard.state.uid) {
|
|
return undefined;
|
|
}
|
|
|
|
const grafanaQueries = await dataQueriesToGrafanaQueries(
|
|
queries,
|
|
rangeUtil.timeRangeToRelative(rangeUtil.convertRawToRange(timeRange.state.value.raw)),
|
|
{ __sceneObject: { value: vizPanel } },
|
|
datasource,
|
|
maxDataPoints,
|
|
minInterval
|
|
);
|
|
|
|
// if no alerting capable queries are found, can't create a rule
|
|
if (!grafanaQueries.length || !grafanaQueries.find((query) => query.datasourceUid !== ExpressionDatasourceUID)) {
|
|
return undefined;
|
|
}
|
|
|
|
if (!grafanaQueries.find((query) => query.datasourceUid === ExpressionDatasourceUID)) {
|
|
const [reduceExpression, _thresholdExpression] = getDefaultExpressions(getNextRefId(grafanaQueries), '-');
|
|
grafanaQueries.push(reduceExpression);
|
|
|
|
const [_reduceExpression, thresholdExpression] = getDefaultExpressions(
|
|
reduceExpression.refId,
|
|
getNextRefId(grafanaQueries)
|
|
);
|
|
grafanaQueries.push(thresholdExpression);
|
|
}
|
|
|
|
const { folderTitle, folderUid } = dashboard.state.meta;
|
|
|
|
const folder =
|
|
folderUid && folderTitle
|
|
? {
|
|
kind: 'folder',
|
|
uid: folderUid,
|
|
title: folderTitle,
|
|
}
|
|
: undefined;
|
|
|
|
const formValues = {
|
|
type: RuleFormType.grafana,
|
|
folder,
|
|
queries: grafanaQueries,
|
|
name: vizPanel.state.title,
|
|
condition: grafanaQueries[grafanaQueries.length - 1].refId,
|
|
annotations: [
|
|
{
|
|
key: Annotation.dashboardUID,
|
|
value: dashboard.state.uid,
|
|
},
|
|
{
|
|
key: Annotation.panelID,
|
|
|
|
value: String(getPanelIdForVizPanel(vizPanel)),
|
|
},
|
|
],
|
|
};
|
|
|
|
return formValues;
|
|
};
|
|
|
|
export function getIntervals(range: TimeRange, lowLimit?: string, resolution?: number): IntervalValues {
|
|
if (!resolution) {
|
|
if (lowLimit && rangeUtil.intervalToMs(lowLimit) > 1000) {
|
|
return {
|
|
interval: lowLimit,
|
|
intervalMs: rangeUtil.intervalToMs(lowLimit),
|
|
};
|
|
}
|
|
return { interval: '1s', intervalMs: 1000 };
|
|
}
|
|
|
|
return rangeUtil.calculateInterval(range, resolution, lowLimit);
|
|
}
|
|
|
|
export function fixBothInstantAndRangeQuery(query: AlertQuery) {
|
|
const model = query.model;
|
|
|
|
if (!isPromQuery(model)) {
|
|
return query;
|
|
}
|
|
|
|
const isBothInstantAndRange = model.instant && model.range;
|
|
if (isBothInstantAndRange) {
|
|
return { ...query, model: { ...model, range: true, instant: false } };
|
|
}
|
|
|
|
return query;
|
|
}
|
|
|
|
function isPromQuery(model: AlertDataQuery): model is PromQuery {
|
|
return 'expr' in model && 'instant' in model && 'range' in model;
|
|
}
|
|
|
|
export function isPromOrLokiQuery(model: AlertDataQuery): model is PromOrLokiQuery {
|
|
return 'expr' in model;
|
|
}
|
|
|
|
// the backend will always execute "hidden" queries, so we have no choice but to remove the property in the front-end
|
|
// to avoid confusion. The query editor shows them as "disabled" and that's a different semantic meaning.
|
|
// furthermore the "AlertingQueryRunner" calls `filterQuery` on each data source and those will skip running queries that are "hidden"."
|
|
// It seems like we have no choice but to act like "hidden" queries don't exist in alerting.
|
|
export const ignoreHiddenQueries = (ruleDefinition: RuleFormValues): RuleFormValues => {
|
|
return {
|
|
...ruleDefinition,
|
|
queries: ruleDefinition.queries?.map((query) => omit(query, 'model.hide')),
|
|
};
|
|
};
|
|
|
|
export function formValuesFromExistingRule(rule: RuleWithLocation<RulerRuleDTO>) {
|
|
return ignoreHiddenQueries(rulerRuleToFormValues(rule));
|
|
}
|
|
|
|
export function getInstantFromDataQuery(model: AlertDataQuery, type: string): boolean | undefined {
|
|
// if the datasource is not prometheus or loki, instant is defined in the model or defaults to undefined
|
|
if (type !== DataSourceType.Prometheus && type !== DataSourceType.Loki) {
|
|
if ('instant' in model) {
|
|
return model.instant;
|
|
} else {
|
|
if ('queryType' in model) {
|
|
return model.queryType === 'instant';
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}
|
|
}
|
|
// if the datasource is prometheus or loki, instant is defined in the model, or defaults to true
|
|
const isInstantForPrometheus = 'instant' in model && model.instant !== undefined ? model.instant : true;
|
|
const isInstantForLoki = 'queryType' in model && model.queryType !== undefined ? model.queryType === 'instant' : true;
|
|
|
|
const isInstant = type === DataSourceType.Prometheus ? isInstantForPrometheus : isInstantForLoki;
|
|
return isInstant;
|
|
}
|