AlertTabCtrl.ts 13 KB

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