The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/alerting/AlertTabCtrl.ts

526 lines
15 KiB

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);