rule-form.ts 11 KB

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