1import {
2  DataQuery,
3  rangeUtil,
4  RelativeTimeRange,
5  ScopedVars,
6  getDefaultRelativeTimeRange,
7  TimeRange,
8  IntervalValues,
9  DataSourceRef,
10} from '@grafana/data';
11import { getDataSourceSrv } from '@grafana/runtime';
12import { contextSrv } from 'app/core/services/context_srv';
13import { getNextRefIdChar } from 'app/core/utils/query';
14import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
15import { ExpressionDatasourceUID } from 'app/features/expressions/ExpressionDatasource';
16import { ExpressionQuery, ExpressionQueryType } from 'app/features/expressions/types';
17import { RuleWithLocation } from 'app/types/unified-alerting';
18import {
19  Annotations,
20  GrafanaAlertStateDecision,
21  AlertQuery,
22  Labels,
23  PostableRuleGrafanaRuleDTO,
24  RulerRuleDTO,
25} from 'app/types/unified-alerting-dto';
26import { EvalFunction } from '../../state/alertDef';
27import { RuleFormType, RuleFormValues } from '../types/rule-form';
28import { Annotation } from './constants';
29import { isGrafanaRulesSource } from './datasource';
30import { arrayToRecord, recordToArray } from './misc';
31import { isAlertingRulerRule, isGrafanaRulerRule, isRecordingRulerRule } from './rules';
32import { parseInterval } from './time';
33import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend';
34
35export const getDefaultFormValues = (): RuleFormValues =>
36  Object.freeze({
37    name: '',
38    labels: [{ key: '', value: '' }],
39    annotations: [
40      { key: Annotation.summary, value: '' },
41      { key: Annotation.description, value: '' },
42      { key: Annotation.runbookURL, value: '' },
43    ],
44    dataSourceName: null,
45    type: !contextSrv.isEditor ? RuleFormType.grafana : undefined, // viewers can't create prom alerts
46
47    // grafana
48    folder: null,
49    queries: [],
50    condition: '',
51    noDataState: GrafanaAlertStateDecision.NoData,
52    execErrState: GrafanaAlertStateDecision.Alerting,
53    evaluateEvery: '1m',
54    evaluateFor: '5m',
55
56    // cortex / loki
57    group: '',
58    namespace: '',
59    expression: '',
60    forTime: 1,
61    forTimeUnit: 'm',
62  });
63
64export function formValuesToRulerRuleDTO(values: RuleFormValues): RulerRuleDTO {
65  const { name, expression, forTime, forTimeUnit, type } = values;
66  if (type === RuleFormType.cloudAlerting) {
67    return {
68      alert: name,
69      for: `${forTime}${forTimeUnit}`,
70      annotations: arrayToRecord(values.annotations || []),
71      labels: arrayToRecord(values.labels || []),
72      expr: expression,
73    };
74  } else if (type === RuleFormType.cloudRecording) {
75    return {
76      record: name,
77      labels: arrayToRecord(values.labels || []),
78      expr: expression,
79    };
80  }
81  throw new Error(`unexpected rule type: ${type}`);
82}
83
84function listifyLabelsOrAnnotations(item: Labels | Annotations | undefined): Array<{ key: string; value: string }> {
85  return [...recordToArray(item || {}), { key: '', value: '' }];
86}
87
88export function formValuesToRulerGrafanaRuleDTO(values: RuleFormValues): PostableRuleGrafanaRuleDTO {
89  const { name, condition, noDataState, execErrState, evaluateFor, queries } = values;
90  if (condition) {
91    return {
92      grafana_alert: {
93        title: name,
94        condition,
95        no_data_state: noDataState,
96        exec_err_state: execErrState,
97        data: queries,
98      },
99      for: evaluateFor,
100      annotations: arrayToRecord(values.annotations || []),
101      labels: arrayToRecord(values.labels || []),
102    };
103  }
104  throw new Error('Cannot create rule without specifying alert condition');
105}
106
107export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleFormValues {
108  const { ruleSourceName, namespace, group, rule } = ruleWithLocation;
109
110  const defaultFormValues = getDefaultFormValues();
111  if (isGrafanaRulesSource(ruleSourceName)) {
112    if (isGrafanaRulerRule(rule)) {
113      const ga = rule.grafana_alert;
114      return {
115        ...defaultFormValues,
116        name: ga.title,
117        type: RuleFormType.grafana,
118        evaluateFor: rule.for || '0',
119        evaluateEvery: group.interval || defaultFormValues.evaluateEvery,
120        noDataState: ga.no_data_state,
121        execErrState: ga.exec_err_state,
122        queries: ga.data,
123        condition: ga.condition,
124        annotations: listifyLabelsOrAnnotations(rule.annotations),
125        labels: listifyLabelsOrAnnotations(rule.labels),
126        folder: { title: namespace, id: ga.namespace_id },
127      };
128    } else {
129      throw new Error('Unexpected type of rule for grafana rules source');
130    }
131  } else {
132    if (isAlertingRulerRule(rule)) {
133      const [forTime, forTimeUnit] = rule.for
134        ? parseInterval(rule.for)
135        : [defaultFormValues.forTime, defaultFormValues.forTimeUnit];
136      return {
137        ...defaultFormValues,
138        name: rule.alert,
139        type: RuleFormType.cloudAlerting,
140        dataSourceName: ruleSourceName,
141        namespace,
142        group: group.name,
143        expression: rule.expr,
144        forTime,
145        forTimeUnit,
146        annotations: listifyLabelsOrAnnotations(rule.annotations),
147        labels: listifyLabelsOrAnnotations(rule.labels),
148      };
149    } else if (isRecordingRulerRule(rule)) {
150      return {
151        ...defaultFormValues,
152        name: rule.record,
153        type: RuleFormType.cloudRecording,
154        dataSourceName: ruleSourceName,
155        namespace,
156        group: group.name,
157        expression: rule.expr,
158        labels: listifyLabelsOrAnnotations(rule.labels),
159      };
160    } else {
161      throw new Error('Unexpected type of rule for cloud rules source');
162    }
163  }
164}
165
166export const getDefaultQueries = (): AlertQuery[] => {
167  const dataSource = getDataSourceSrv().getInstanceSettings('default');
168
169  if (!dataSource) {
170    return [getDefaultExpression('A')];
171  }
172  const relativeTimeRange = getDefaultRelativeTimeRange();
173
174  return [
175    {
176      refId: 'A',
177      datasourceUid: dataSource.uid,
178      queryType: '',
179      relativeTimeRange,
180      model: {
181        refId: 'A',
182        hide: false,
183      },
184    },
185    getDefaultExpression('B'),
186  ];
187};
188
189const getDefaultExpression = (refId: string): AlertQuery => {
190  const model: ExpressionQuery = {
191    refId,
192    hide: false,
193    type: ExpressionQueryType.classic,
194    datasource: {
195      uid: ExpressionDatasourceUID,
196      type: ExpressionDatasourceRef.type,
197    },
198    conditions: [
199      {
200        type: 'query',
201        evaluator: {
202          params: [3],
203          type: EvalFunction.IsAbove,
204        },
205        operator: {
206          type: 'and',
207        },
208        query: {
209          params: ['A'],
210        },
211        reducer: {
212          params: [],
213          type: 'last',
214        },
215      },
216    ],
217  };
218
219  return {
220    refId,
221    datasourceUid: ExpressionDatasourceUID,
222    queryType: '',
223    model,
224  };
225};
226
227const dataQueriesToGrafanaQueries = async (
228  queries: DataQuery[],
229  relativeTimeRange: RelativeTimeRange,
230  scopedVars: ScopedVars | {},
231  panelDataSourceRef?: DataSourceRef,
232  maxDataPoints?: number,
233  minInterval?: string
234): Promise<AlertQuery[]> => {
235  const result: AlertQuery[] = [];
236
237  for (const target of queries) {
238    const datasource = await getDataSourceSrv().get(target.datasource?.uid ? target.datasource : panelDataSourceRef);
239    const dsRef = { uid: datasource.uid, type: datasource.type };
240
241    const range = rangeUtil.relativeToTimeRange(relativeTimeRange);
242    const { interval, intervalMs } = getIntervals(range, minInterval ?? datasource.interval, maxDataPoints);
243    const queryVariables = {
244      __interval: { text: interval, value: interval },
245      __interval_ms: { text: intervalMs, value: intervalMs },
246      ...scopedVars,
247    };
248
249    const interpolatedTarget = datasource.interpolateVariablesInQueries
250      ? await datasource.interpolateVariablesInQueries([target], queryVariables)[0]
251      : target;
252
253    // expressions
254    if (dsRef.uid === ExpressionDatasourceUID) {
255      const newQuery: AlertQuery = {
256        refId: interpolatedTarget.refId,
257        queryType: '',
258        relativeTimeRange,
259        datasourceUid: ExpressionDatasourceUID,
260        model: interpolatedTarget,
261      };
262      result.push(newQuery);
263      // queries
264    } else {
265      const datasourceSettings = getDataSourceSrv().getInstanceSettings(dsRef);
266      if (datasourceSettings && datasourceSettings.meta.alerting) {
267        const newQuery: AlertQuery = {
268          refId: interpolatedTarget.refId,
269          queryType: interpolatedTarget.queryType ?? '',
270          relativeTimeRange,
271          datasourceUid: datasourceSettings.uid,
272          model: {
273            ...interpolatedTarget,
274            maxDataPoints,
275            intervalMs,
276          },
277        };
278        result.push(newQuery);
279      }
280    }
281  }
282  return result;
283};
284
285export const panelToRuleFormValues = async (
286  panel: PanelModel,
287  dashboard: DashboardModel
288): Promise<Partial<RuleFormValues> | undefined> => {
289  const { targets } = panel;
290  if (!panel.id || !dashboard.uid) {
291    return undefined;
292  }
293
294  const relativeTimeRange = rangeUtil.timeRangeToRelative(rangeUtil.convertRawToRange(dashboard.time));
295  const queries = await dataQueriesToGrafanaQueries(
296    targets,
297    relativeTimeRange,
298    panel.scopedVars || {},
299    panel.datasource ?? undefined,
300    panel.maxDataPoints ?? undefined,
301    panel.interval ?? undefined
302  );
303  // if no alerting capable queries are found, can't create a rule
304  if (!queries.length || !queries.find((query) => query.datasourceUid !== ExpressionDatasourceUID)) {
305    return undefined;
306  }
307
308  if (!queries.find((query) => query.datasourceUid === ExpressionDatasourceUID)) {
309    queries.push(getDefaultExpression(getNextRefIdChar(queries.map((query) => query.model))));
310  }
311
312  const { folderId, folderTitle } = dashboard.meta;
313
314  const formValues = {
315    type: RuleFormType.grafana,
316    folder:
317      folderId && folderTitle
318        ? {
319            id: folderId,
320            title: folderTitle,
321          }
322        : undefined,
323    queries,
324    name: panel.title,
325    condition: queries[queries.length - 1].refId,
326    annotations: [
327      {
328        key: Annotation.dashboardUID,
329        value: dashboard.uid,
330      },
331      {
332        key: Annotation.panelID,
333        value: String(panel.id),
334      },
335    ],
336  };
337  return formValues;
338};
339
340export function getIntervals(range: TimeRange, lowLimit?: string, resolution?: number): IntervalValues {
341  if (!resolution) {
342    if (lowLimit && rangeUtil.intervalToMs(lowLimit) > 1000) {
343      return {
344        interval: lowLimit,
345        intervalMs: rangeUtil.intervalToMs(lowLimit),
346      };
347    }
348    return { interval: '1s', intervalMs: 1000 };
349  }
350
351  return rangeUtil.calculateInterval(range, resolution, lowLimit);
352}
353