AlertTabCtrl.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. import { find, map, reduce, remove } from 'lodash';
  2. import { DataQuery, DataSourceApi, rangeUtil } from '@grafana/data';
  3. import { getBackendSrv } from '@grafana/runtime';
  4. import coreModule from 'app/angular/core_module';
  5. import { promiseToDigest } from 'app/angular/promiseToDigest';
  6. import appEvents from 'app/core/app_events';
  7. import config from 'app/core/config';
  8. import { QueryPart } from 'app/features/alerting/state/query_part';
  9. import { PanelModel } from 'app/features/dashboard/state';
  10. import { CoreEvents } from 'app/types';
  11. import { ShowConfirmModalEvent } from '../../types/events';
  12. import { DashboardSrv } from '../dashboard/services/DashboardSrv';
  13. import { DatasourceSrv } from '../plugins/datasource_srv';
  14. import { getDefaultCondition } from './getAlertingValidationMessage';
  15. import { ThresholdMapper } from './state/ThresholdMapper';
  16. import alertDef from './state/alertDef';
  17. export class AlertTabCtrl {
  18. panel: PanelModel;
  19. panelCtrl: any;
  20. subTabIndex: number;
  21. conditionTypes: any;
  22. alert: any;
  23. conditionModels: any;
  24. evalFunctions: any;
  25. evalOperators: any;
  26. noDataModes: any;
  27. executionErrorModes: any;
  28. addNotificationSegment: any;
  29. notifications: any;
  30. alertNotifications: any;
  31. error?: string;
  32. appSubUrl: string;
  33. alertHistory: any;
  34. newAlertRuleTag: any;
  35. alertingMinIntervalSecs: number;
  36. alertingMinInterval: string;
  37. frequencyWarning: any;
  38. /** @ngInject */
  39. constructor(
  40. private $scope: any,
  41. private dashboardSrv: DashboardSrv,
  42. private uiSegmentSrv: any,
  43. private datasourceSrv: DatasourceSrv
  44. ) {
  45. this.panelCtrl = $scope.ctrl;
  46. this.panel = this.panelCtrl.panel;
  47. this.$scope.ctrl = this;
  48. this.subTabIndex = 0;
  49. this.evalFunctions = alertDef.evalFunctions;
  50. this.evalOperators = alertDef.evalOperators;
  51. this.conditionTypes = alertDef.conditionTypes;
  52. this.noDataModes = alertDef.noDataModes;
  53. this.executionErrorModes = alertDef.executionErrorModes;
  54. this.appSubUrl = config.appSubUrl;
  55. this.panelCtrl._enableAlert = this.enable;
  56. this.alertingMinIntervalSecs = config.alertingMinInterval;
  57. this.alertingMinInterval = rangeUtil.secondsToHms(config.alertingMinInterval);
  58. }
  59. $onInit() {
  60. this.addNotificationSegment = this.uiSegmentSrv.newPlusButton();
  61. // subscribe to graph threshold handle changes
  62. const thresholdChangedEventHandler = this.graphThresholdChanged.bind(this);
  63. this.panelCtrl.events.on(CoreEvents.thresholdChanged, thresholdChangedEventHandler);
  64. // set panel alert edit mode
  65. this.$scope.$on('$destroy', () => {
  66. this.panelCtrl.events.off(CoreEvents.thresholdChanged, thresholdChangedEventHandler);
  67. this.panelCtrl.editingThresholds = false;
  68. this.panelCtrl.render();
  69. });
  70. // build notification model
  71. this.notifications = [];
  72. this.alertNotifications = [];
  73. this.alertHistory = [];
  74. return promiseToDigest(this.$scope)(
  75. getBackendSrv()
  76. .get('/api/alert-notifications/lookup')
  77. .then((res: any) => {
  78. this.notifications = res;
  79. this.initModel();
  80. this.validateModel();
  81. })
  82. );
  83. }
  84. getAlertHistory() {
  85. promiseToDigest(this.$scope)(
  86. getBackendSrv()
  87. .get(`/api/annotations?dashboardId=${this.panelCtrl.dashboard.id}&panelId=${this.panel.id}&limit=50&type=alert`)
  88. .then((res: any) => {
  89. this.alertHistory = map(res, (ah) => {
  90. ah.time = this.dashboardSrv.getCurrent()?.formatDate(ah.time, 'MMM D, YYYY HH:mm:ss');
  91. ah.stateModel = alertDef.getStateDisplayModel(ah.newState);
  92. ah.info = alertDef.getAlertAnnotationInfo(ah);
  93. return ah;
  94. });
  95. })
  96. );
  97. }
  98. getNotificationIcon(type: string): string {
  99. switch (type) {
  100. case 'email':
  101. return 'envelope';
  102. case 'slack':
  103. return 'slack';
  104. case 'victorops':
  105. return 'fa fa-pagelines';
  106. case 'webhook':
  107. return 'cube';
  108. case 'pagerduty':
  109. return 'fa fa-bullhorn';
  110. case 'opsgenie':
  111. return 'bell';
  112. case 'hipchat':
  113. return 'fa fa-mail-forward';
  114. case 'pushover':
  115. return 'mobile-android';
  116. case 'kafka':
  117. return 'arrow-random';
  118. case 'teams':
  119. return 'fa fa-windows';
  120. }
  121. return 'bell';
  122. }
  123. getNotifications() {
  124. return Promise.resolve(
  125. this.notifications.map((item: any) => {
  126. return this.uiSegmentSrv.newSegment(item.name);
  127. })
  128. );
  129. }
  130. notificationAdded() {
  131. const model: any = find(this.notifications, {
  132. name: this.addNotificationSegment.value,
  133. });
  134. if (!model) {
  135. return;
  136. }
  137. this.alertNotifications.push({
  138. name: model.name,
  139. iconClass: this.getNotificationIcon(model.type),
  140. isDefault: false,
  141. uid: model.uid,
  142. });
  143. // avoid duplicates using both id and uid to be backwards compatible.
  144. if (!find(this.alert.notifications, (n) => n.id === model.id || n.uid === model.uid)) {
  145. this.alert.notifications.push({ uid: model.uid });
  146. }
  147. // reset plus button
  148. this.addNotificationSegment.value = this.uiSegmentSrv.newPlusButton().value;
  149. this.addNotificationSegment.html = this.uiSegmentSrv.newPlusButton().html;
  150. this.addNotificationSegment.fake = true;
  151. }
  152. removeNotification(an: any) {
  153. // remove notifiers referred to by id and uid to support notifiers added
  154. // before and after we added support for uid
  155. remove(this.alert.notifications, (n: any) => n.uid === an.uid || n.id === an.id);
  156. remove(this.alertNotifications, (n: any) => n.uid === an.uid || n.id === an.id);
  157. }
  158. addAlertRuleTag() {
  159. if (this.newAlertRuleTag.name) {
  160. this.alert.alertRuleTags[this.newAlertRuleTag.name] = this.newAlertRuleTag.value;
  161. }
  162. this.newAlertRuleTag.name = '';
  163. this.newAlertRuleTag.value = '';
  164. }
  165. removeAlertRuleTag(tagName: string) {
  166. delete this.alert.alertRuleTags[tagName];
  167. }
  168. initModel() {
  169. const alert = (this.alert = this.panel.alert);
  170. if (!alert) {
  171. return;
  172. }
  173. this.checkFrequency();
  174. alert.conditions = alert.conditions || [];
  175. if (alert.conditions.length === 0) {
  176. alert.conditions.push(getDefaultCondition());
  177. }
  178. alert.noDataState = alert.noDataState || config.alertingNoDataOrNullValues;
  179. alert.executionErrorState = alert.executionErrorState || config.alertingErrorOrTimeout;
  180. alert.frequency = alert.frequency || '1m';
  181. alert.handler = alert.handler || 1;
  182. alert.notifications = alert.notifications || [];
  183. alert.for = alert.for || '0m';
  184. alert.alertRuleTags = alert.alertRuleTags || {};
  185. const defaultName = this.panel.title + ' alert';
  186. alert.name = alert.name || defaultName;
  187. this.conditionModels = reduce(
  188. alert.conditions,
  189. (memo, value) => {
  190. memo.push(this.buildConditionModel(value));
  191. return memo;
  192. },
  193. [] as string[]
  194. );
  195. ThresholdMapper.alertToGraphThresholds(this.panel);
  196. for (const addedNotification of alert.notifications) {
  197. let identifier = addedNotification.uid;
  198. // lookup notifier type by uid
  199. let model: any = find(this.notifications, { uid: identifier });
  200. // fallback using id if uid is missing
  201. if (!model && addedNotification.id) {
  202. identifier = addedNotification.id;
  203. model = find(this.notifications, { id: identifier });
  204. }
  205. if (!model) {
  206. appEvents.publish(
  207. new ShowConfirmModalEvent({
  208. title: 'Notifier with invalid identifier is detected',
  209. text: `Do you want to delete notifier with invalid identifier: ${identifier} from the dashboard JSON?`,
  210. text2: 'After successful deletion, make sure to save the dashboard for storing the update JSON.',
  211. icon: 'trash-alt',
  212. confirmText: 'Delete',
  213. yesText: 'Delete',
  214. onConfirm: async () => {
  215. this.removeNotification(addedNotification);
  216. },
  217. })
  218. );
  219. }
  220. if (model && model.isDefault === false) {
  221. model.iconClass = this.getNotificationIcon(model.type);
  222. this.alertNotifications.push(model);
  223. }
  224. }
  225. for (const notification of this.notifications) {
  226. if (notification.isDefault) {
  227. notification.iconClass = this.getNotificationIcon(notification.type);
  228. this.alertNotifications.push(notification);
  229. }
  230. }
  231. this.panelCtrl.editingThresholds = true;
  232. this.panelCtrl.render();
  233. }
  234. checkFrequency() {
  235. this.frequencyWarning = '';
  236. if (!this.alert.frequency) {
  237. return;
  238. }
  239. if (!this.alert.frequency.match(/^\d+([dhms])$/)) {
  240. this.frequencyWarning =
  241. 'Invalid frequency, has to be numeric followed by one of the following units: "d, h, m, s"';
  242. return;
  243. }
  244. try {
  245. const frequencySecs = rangeUtil.intervalToSeconds(this.alert.frequency);
  246. if (frequencySecs < this.alertingMinIntervalSecs) {
  247. this.frequencyWarning =
  248. 'A minimum evaluation interval of ' +
  249. this.alertingMinInterval +
  250. ' have been configured in Grafana and will be used for this alert rule. ' +
  251. 'Please contact the administrator to configure a lower interval.';
  252. }
  253. } catch (err) {
  254. this.frequencyWarning = err;
  255. }
  256. }
  257. graphThresholdChanged(evt: any) {
  258. for (const condition of this.alert.conditions) {
  259. if (condition.type === 'query') {
  260. condition.evaluator.params[evt.handleIndex] = evt.threshold.value;
  261. this.evaluatorParamsChanged();
  262. break;
  263. }
  264. }
  265. }
  266. validateModel() {
  267. if (!this.alert) {
  268. return;
  269. }
  270. let firstTarget;
  271. let foundTarget: DataQuery | null = null;
  272. const promises: Array<Promise<any>> = [];
  273. for (const condition of this.alert.conditions) {
  274. if (condition.type !== 'query') {
  275. continue;
  276. }
  277. for (const target of this.panel.targets) {
  278. if (!firstTarget) {
  279. firstTarget = target;
  280. }
  281. if (condition.query.params[0] === target.refId) {
  282. foundTarget = target;
  283. break;
  284. }
  285. }
  286. if (!foundTarget) {
  287. if (firstTarget) {
  288. condition.query.params[0] = firstTarget.refId;
  289. foundTarget = firstTarget;
  290. } else {
  291. this.error = 'Could not find any metric queries';
  292. return;
  293. }
  294. }
  295. const datasourceName = foundTarget.datasource || this.panel.datasource;
  296. promises.push(
  297. this.datasourceSrv.get(datasourceName).then(
  298. ((foundTarget) => (ds: DataSourceApi) => {
  299. if (!ds.meta.alerting) {
  300. return Promise.reject('The datasource does not support alerting queries');
  301. } else if (ds.targetContainsTemplate && ds.targetContainsTemplate(foundTarget)) {
  302. return Promise.reject('Template variables are not supported in alert queries');
  303. }
  304. return Promise.resolve();
  305. })(foundTarget)
  306. )
  307. );
  308. }
  309. Promise.all(promises).then(
  310. () => {
  311. this.error = '';
  312. this.$scope.$apply();
  313. },
  314. (e) => {
  315. this.error = e;
  316. this.$scope.$apply();
  317. }
  318. );
  319. }
  320. buildConditionModel(source: any) {
  321. const cm: any = { source: source, type: source.type };
  322. cm.queryPart = new QueryPart(source.query, alertDef.alertQueryDef);
  323. cm.reducerPart = alertDef.createReducerPart(source.reducer);
  324. cm.evaluator = source.evaluator;
  325. cm.operator = source.operator;
  326. return cm;
  327. }
  328. handleQueryPartEvent(conditionModel: any, evt: any) {
  329. switch (evt.name) {
  330. case 'action-remove-part': {
  331. break;
  332. }
  333. case 'get-part-actions': {
  334. return Promise.resolve([]);
  335. }
  336. case 'part-param-changed': {
  337. this.validateModel();
  338. }
  339. case 'get-param-options': {
  340. const result = this.panel.targets.map((target) => {
  341. return this.uiSegmentSrv.newSegment({ value: target.refId });
  342. });
  343. return Promise.resolve(result);
  344. }
  345. default: {
  346. return Promise.resolve();
  347. }
  348. }
  349. return Promise.resolve();
  350. }
  351. handleReducerPartEvent(conditionModel: any, evt: any) {
  352. switch (evt.name) {
  353. case 'action': {
  354. conditionModel.source.reducer.type = evt.action.value;
  355. conditionModel.reducerPart = alertDef.createReducerPart(conditionModel.source.reducer);
  356. this.evaluatorParamsChanged();
  357. break;
  358. }
  359. case 'get-part-actions': {
  360. const result = [];
  361. for (const type of alertDef.reducerTypes) {
  362. if (type.value !== conditionModel.source.reducer.type) {
  363. result.push(type);
  364. }
  365. }
  366. return Promise.resolve(result);
  367. }
  368. }
  369. return Promise.resolve();
  370. }
  371. addCondition(type: string) {
  372. const condition = getDefaultCondition();
  373. // add to persited model
  374. this.alert.conditions.push(condition);
  375. // add to view model
  376. this.conditionModels.push(this.buildConditionModel(condition));
  377. }
  378. removeCondition(index: number) {
  379. this.alert.conditions.splice(index, 1);
  380. this.conditionModels.splice(index, 1);
  381. }
  382. delete() {
  383. appEvents.publish(
  384. new ShowConfirmModalEvent({
  385. title: 'Delete Alert',
  386. text: 'Are you sure you want to delete this alert rule?',
  387. text2: 'You need to save dashboard for the delete to take effect',
  388. icon: 'trash-alt',
  389. yesText: 'Delete',
  390. onConfirm: () => {
  391. delete this.panel.alert;
  392. this.alert = null;
  393. this.panel.thresholds = [];
  394. this.conditionModels = [];
  395. this.panelCtrl.alertState = null;
  396. this.panelCtrl.render();
  397. },
  398. })
  399. );
  400. }
  401. enable = () => {
  402. this.panel.alert = {};
  403. this.initModel();
  404. this.panel.alert.for = '5m'; //default value for new alerts. for existing alerts we use 0m to avoid breaking changes
  405. };
  406. evaluatorParamsChanged() {
  407. ThresholdMapper.alertToGraphThresholds(this.panel);
  408. this.panelCtrl.render();
  409. }
  410. evaluatorTypeChanged(evaluator: any) {
  411. // ensure params array is correct length
  412. switch (evaluator.type) {
  413. case 'lt':
  414. case 'gt': {
  415. evaluator.params = [evaluator.params[0]];
  416. break;
  417. }
  418. case 'within_range':
  419. case 'outside_range': {
  420. evaluator.params = [evaluator.params[0], evaluator.params[1]];
  421. break;
  422. }
  423. case 'no_value': {
  424. evaluator.params = [];
  425. }
  426. }
  427. this.evaluatorParamsChanged();
  428. }
  429. clearHistory() {
  430. appEvents.publish(
  431. new ShowConfirmModalEvent({
  432. title: 'Delete Alert History',
  433. text: 'Are you sure you want to remove all history & annotations for this alert?',
  434. icon: 'trash-alt',
  435. yesText: 'Yes',
  436. onConfirm: () => {
  437. promiseToDigest(this.$scope)(
  438. getBackendSrv()
  439. .post('/api/annotations/mass-delete', {
  440. dashboardId: this.panelCtrl.dashboard.id,
  441. panelId: this.panel.id,
  442. })
  443. .then(() => {
  444. this.alertHistory = [];
  445. this.panelCtrl.refresh();
  446. })
  447. );
  448. },
  449. })
  450. );
  451. }
  452. }
  453. /** @ngInject */
  454. export function alertTab() {
  455. 'use strict';
  456. return {
  457. restrict: 'E',
  458. scope: true,
  459. templateUrl: 'public/app/features/alerting/partials/alert_tab.html',
  460. controller: AlertTabCtrl,
  461. };
  462. }
  463. coreModule.directive('alertTab', alertTab);