123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526 |
- import { find, map, reduce, remove } from 'lodash';
- import { DataQuery, DataSourceApi, rangeUtil } from '@grafana/data';
- import { getBackendSrv } from '@grafana/runtime';
- import coreModule from 'app/angular/core_module';
- import { promiseToDigest } from 'app/angular/promiseToDigest';
- import appEvents from 'app/core/app_events';
- import config from 'app/core/config';
- import { QueryPart } from 'app/features/alerting/state/query_part';
- import { PanelModel } from 'app/features/dashboard/state';
- import { CoreEvents } from 'app/types';
- import { ShowConfirmModalEvent } from '../../types/events';
- import { DashboardSrv } from '../dashboard/services/DashboardSrv';
- import { DatasourceSrv } from '../plugins/datasource_srv';
- import { getDefaultCondition } from './getAlertingValidationMessage';
- import { ThresholdMapper } from './state/ThresholdMapper';
- import alertDef from './state/alertDef';
- export class AlertTabCtrl {
- panel: PanelModel;
- panelCtrl: any;
- subTabIndex: number;
- conditionTypes: any;
- alert: any;
- conditionModels: any;
- evalFunctions: any;
- evalOperators: any;
- noDataModes: any;
- executionErrorModes: any;
- addNotificationSegment: any;
- notifications: any;
- alertNotifications: any;
- error?: string;
- appSubUrl: string;
- alertHistory: any;
- newAlertRuleTag: any;
- alertingMinIntervalSecs: number;
- alertingMinInterval: string;
- frequencyWarning: any;
- /** @ngInject */
- constructor(
- private $scope: any,
- private dashboardSrv: DashboardSrv,
- private uiSegmentSrv: any,
- private datasourceSrv: DatasourceSrv
- ) {
- this.panelCtrl = $scope.ctrl;
- this.panel = this.panelCtrl.panel;
- this.$scope.ctrl = this;
- this.subTabIndex = 0;
- this.evalFunctions = alertDef.evalFunctions;
- this.evalOperators = alertDef.evalOperators;
- this.conditionTypes = alertDef.conditionTypes;
- this.noDataModes = alertDef.noDataModes;
- this.executionErrorModes = alertDef.executionErrorModes;
- this.appSubUrl = config.appSubUrl;
- this.panelCtrl._enableAlert = this.enable;
- this.alertingMinIntervalSecs = config.alertingMinInterval;
- this.alertingMinInterval = rangeUtil.secondsToHms(config.alertingMinInterval);
- }
- $onInit() {
- this.addNotificationSegment = this.uiSegmentSrv.newPlusButton();
- // subscribe to graph threshold handle changes
- const thresholdChangedEventHandler = this.graphThresholdChanged.bind(this);
- this.panelCtrl.events.on(CoreEvents.thresholdChanged, thresholdChangedEventHandler);
- // set panel alert edit mode
- this.$scope.$on('$destroy', () => {
- this.panelCtrl.events.off(CoreEvents.thresholdChanged, thresholdChangedEventHandler);
- this.panelCtrl.editingThresholds = false;
- this.panelCtrl.render();
- });
- // build notification model
- this.notifications = [];
- this.alertNotifications = [];
- this.alertHistory = [];
- return promiseToDigest(this.$scope)(
- getBackendSrv()
- .get('/api/alert-notifications/lookup')
- .then((res: any) => {
- this.notifications = res;
- this.initModel();
- this.validateModel();
- })
- );
- }
- getAlertHistory() {
- promiseToDigest(this.$scope)(
- getBackendSrv()
- .get(`/api/annotations?dashboardId=${this.panelCtrl.dashboard.id}&panelId=${this.panel.id}&limit=50&type=alert`)
- .then((res: any) => {
- this.alertHistory = map(res, (ah) => {
- ah.time = this.dashboardSrv.getCurrent()?.formatDate(ah.time, 'MMM D, YYYY HH:mm:ss');
- ah.stateModel = alertDef.getStateDisplayModel(ah.newState);
- ah.info = alertDef.getAlertAnnotationInfo(ah);
- return ah;
- });
- })
- );
- }
- getNotificationIcon(type: string): string {
- switch (type) {
- case 'email':
- return 'envelope';
- case 'slack':
- return 'slack';
- case 'victorops':
- return 'fa fa-pagelines';
- case 'webhook':
- return 'cube';
- case 'pagerduty':
- return 'fa fa-bullhorn';
- case 'opsgenie':
- return 'bell';
- case 'hipchat':
- return 'fa fa-mail-forward';
- case 'pushover':
- return 'mobile-android';
- case 'kafka':
- return 'arrow-random';
- case 'teams':
- return 'fa fa-windows';
- }
- return 'bell';
- }
- getNotifications() {
- return Promise.resolve(
- this.notifications.map((item: any) => {
- return this.uiSegmentSrv.newSegment(item.name);
- })
- );
- }
- notificationAdded() {
- const model: any = find(this.notifications, {
- name: this.addNotificationSegment.value,
- });
- if (!model) {
- return;
- }
- this.alertNotifications.push({
- name: model.name,
- iconClass: this.getNotificationIcon(model.type),
- isDefault: false,
- uid: model.uid,
- });
- // avoid duplicates using both id and uid to be backwards compatible.
- if (!find(this.alert.notifications, (n) => n.id === model.id || n.uid === model.uid)) {
- this.alert.notifications.push({ uid: model.uid });
- }
- // reset plus button
- this.addNotificationSegment.value = this.uiSegmentSrv.newPlusButton().value;
- this.addNotificationSegment.html = this.uiSegmentSrv.newPlusButton().html;
- this.addNotificationSegment.fake = true;
- }
- removeNotification(an: any) {
- // remove notifiers referred to by id and uid to support notifiers added
- // before and after we added support for uid
- remove(this.alert.notifications, (n: any) => n.uid === an.uid || n.id === an.id);
- remove(this.alertNotifications, (n: any) => n.uid === an.uid || n.id === an.id);
- }
- addAlertRuleTag() {
- if (this.newAlertRuleTag.name) {
- this.alert.alertRuleTags[this.newAlertRuleTag.name] = this.newAlertRuleTag.value;
- }
- this.newAlertRuleTag.name = '';
- this.newAlertRuleTag.value = '';
- }
- removeAlertRuleTag(tagName: string) {
- delete this.alert.alertRuleTags[tagName];
- }
- initModel() {
- const alert = (this.alert = this.panel.alert);
- if (!alert) {
- return;
- }
- this.checkFrequency();
- alert.conditions = alert.conditions || [];
- if (alert.conditions.length === 0) {
- alert.conditions.push(getDefaultCondition());
- }
- alert.noDataState = alert.noDataState || config.alertingNoDataOrNullValues;
- alert.executionErrorState = alert.executionErrorState || config.alertingErrorOrTimeout;
- alert.frequency = alert.frequency || '1m';
- alert.handler = alert.handler || 1;
- alert.notifications = alert.notifications || [];
- alert.for = alert.for || '0m';
- alert.alertRuleTags = alert.alertRuleTags || {};
- const defaultName = this.panel.title + ' alert';
- alert.name = alert.name || defaultName;
- this.conditionModels = reduce(
- alert.conditions,
- (memo, value) => {
- memo.push(this.buildConditionModel(value));
- return memo;
- },
- [] as string[]
- );
- ThresholdMapper.alertToGraphThresholds(this.panel);
- for (const addedNotification of alert.notifications) {
- let identifier = addedNotification.uid;
- // lookup notifier type by uid
- let model: any = find(this.notifications, { uid: identifier });
- // fallback using id if uid is missing
- if (!model && addedNotification.id) {
- identifier = addedNotification.id;
- model = find(this.notifications, { id: identifier });
- }
- if (!model) {
- appEvents.publish(
- new ShowConfirmModalEvent({
- title: 'Notifier with invalid identifier is detected',
- text: `Do you want to delete notifier with invalid identifier: ${identifier} from the dashboard JSON?`,
- text2: 'After successful deletion, make sure to save the dashboard for storing the update JSON.',
- icon: 'trash-alt',
- confirmText: 'Delete',
- yesText: 'Delete',
- onConfirm: async () => {
- this.removeNotification(addedNotification);
- },
- })
- );
- }
- if (model && model.isDefault === false) {
- model.iconClass = this.getNotificationIcon(model.type);
- this.alertNotifications.push(model);
- }
- }
- for (const notification of this.notifications) {
- if (notification.isDefault) {
- notification.iconClass = this.getNotificationIcon(notification.type);
- this.alertNotifications.push(notification);
- }
- }
- this.panelCtrl.editingThresholds = true;
- this.panelCtrl.render();
- }
- checkFrequency() {
- this.frequencyWarning = '';
- if (!this.alert.frequency) {
- return;
- }
- if (!this.alert.frequency.match(/^\d+([dhms])$/)) {
- this.frequencyWarning =
- 'Invalid frequency, has to be numeric followed by one of the following units: "d, h, m, s"';
- return;
- }
- try {
- const frequencySecs = rangeUtil.intervalToSeconds(this.alert.frequency);
- if (frequencySecs < this.alertingMinIntervalSecs) {
- this.frequencyWarning =
- 'A minimum evaluation interval of ' +
- this.alertingMinInterval +
- ' have been configured in Grafana and will be used for this alert rule. ' +
- 'Please contact the administrator to configure a lower interval.';
- }
- } catch (err) {
- this.frequencyWarning = err;
- }
- }
- graphThresholdChanged(evt: any) {
- for (const condition of this.alert.conditions) {
- if (condition.type === 'query') {
- condition.evaluator.params[evt.handleIndex] = evt.threshold.value;
- this.evaluatorParamsChanged();
- break;
- }
- }
- }
- validateModel() {
- if (!this.alert) {
- return;
- }
- let firstTarget;
- let foundTarget: DataQuery | null = null;
- const promises: Array<Promise<any>> = [];
- for (const condition of this.alert.conditions) {
- if (condition.type !== 'query') {
- continue;
- }
- for (const target of this.panel.targets) {
- if (!firstTarget) {
- firstTarget = target;
- }
- if (condition.query.params[0] === target.refId) {
- foundTarget = target;
- break;
- }
- }
- if (!foundTarget) {
- if (firstTarget) {
- condition.query.params[0] = firstTarget.refId;
- foundTarget = firstTarget;
- } else {
- this.error = 'Could not find any metric queries';
- return;
- }
- }
- const datasourceName = foundTarget.datasource || this.panel.datasource;
- promises.push(
- this.datasourceSrv.get(datasourceName).then(
- ((foundTarget) => (ds: DataSourceApi) => {
- if (!ds.meta.alerting) {
- return Promise.reject('The datasource does not support alerting queries');
- } else if (ds.targetContainsTemplate && ds.targetContainsTemplate(foundTarget)) {
- return Promise.reject('Template variables are not supported in alert queries');
- }
- return Promise.resolve();
- })(foundTarget)
- )
- );
- }
- Promise.all(promises).then(
- () => {
- this.error = '';
- this.$scope.$apply();
- },
- (e) => {
- this.error = e;
- this.$scope.$apply();
- }
- );
- }
- buildConditionModel(source: any) {
- const cm: any = { source: source, type: source.type };
- cm.queryPart = new QueryPart(source.query, alertDef.alertQueryDef);
- cm.reducerPart = alertDef.createReducerPart(source.reducer);
- cm.evaluator = source.evaluator;
- cm.operator = source.operator;
- return cm;
- }
- handleQueryPartEvent(conditionModel: any, evt: any) {
- switch (evt.name) {
- case 'action-remove-part': {
- break;
- }
- case 'get-part-actions': {
- return Promise.resolve([]);
- }
- case 'part-param-changed': {
- this.validateModel();
- }
- case 'get-param-options': {
- const result = this.panel.targets.map((target) => {
- return this.uiSegmentSrv.newSegment({ value: target.refId });
- });
- return Promise.resolve(result);
- }
- default: {
- return Promise.resolve();
- }
- }
- return Promise.resolve();
- }
- handleReducerPartEvent(conditionModel: any, evt: any) {
- switch (evt.name) {
- case 'action': {
- conditionModel.source.reducer.type = evt.action.value;
- conditionModel.reducerPart = alertDef.createReducerPart(conditionModel.source.reducer);
- this.evaluatorParamsChanged();
- break;
- }
- case 'get-part-actions': {
- const result = [];
- for (const type of alertDef.reducerTypes) {
- if (type.value !== conditionModel.source.reducer.type) {
- result.push(type);
- }
- }
- return Promise.resolve(result);
- }
- }
- return Promise.resolve();
- }
- addCondition(type: string) {
- const condition = getDefaultCondition();
- // add to persited model
- this.alert.conditions.push(condition);
- // add to view model
- this.conditionModels.push(this.buildConditionModel(condition));
- }
- removeCondition(index: number) {
- this.alert.conditions.splice(index, 1);
- this.conditionModels.splice(index, 1);
- }
- delete() {
- appEvents.publish(
- new ShowConfirmModalEvent({
- title: 'Delete Alert',
- text: 'Are you sure you want to delete this alert rule?',
- text2: 'You need to save dashboard for the delete to take effect',
- icon: 'trash-alt',
- yesText: 'Delete',
- onConfirm: () => {
- delete this.panel.alert;
- this.alert = null;
- this.panel.thresholds = [];
- this.conditionModels = [];
- this.panelCtrl.alertState = null;
- this.panelCtrl.render();
- },
- })
- );
- }
- enable = () => {
- this.panel.alert = {};
- this.initModel();
- this.panel.alert.for = '5m'; //default value for new alerts. for existing alerts we use 0m to avoid breaking changes
- };
- evaluatorParamsChanged() {
- ThresholdMapper.alertToGraphThresholds(this.panel);
- this.panelCtrl.render();
- }
- evaluatorTypeChanged(evaluator: any) {
- // ensure params array is correct length
- switch (evaluator.type) {
- case 'lt':
- case 'gt': {
- evaluator.params = [evaluator.params[0]];
- break;
- }
- case 'within_range':
- case 'outside_range': {
- evaluator.params = [evaluator.params[0], evaluator.params[1]];
- break;
- }
- case 'no_value': {
- evaluator.params = [];
- }
- }
- this.evaluatorParamsChanged();
- }
- clearHistory() {
- appEvents.publish(
- new ShowConfirmModalEvent({
- title: 'Delete Alert History',
- text: 'Are you sure you want to remove all history & annotations for this alert?',
- icon: 'trash-alt',
- yesText: 'Yes',
- onConfirm: () => {
- promiseToDigest(this.$scope)(
- getBackendSrv()
- .post('/api/annotations/mass-delete', {
- dashboardId: this.panelCtrl.dashboard.id,
- panelId: this.panel.id,
- })
- .then(() => {
- this.alertHistory = [];
- this.panelCtrl.refresh();
- })
- );
- },
- })
- );
- }
- }
- /** @ngInject */
- export function alertTab() {
- 'use strict';
- return {
- restrict: 'E',
- scope: true,
- templateUrl: 'public/app/features/alerting/partials/alert_tab.html',
- controller: AlertTabCtrl,
- };
- }
- coreModule.directive('alertTab', alertTab);
|