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

436 lines
12 KiB

import _ from 'lodash';
import coreModule from 'app/core/core_module';
import { ThresholdMapper } from './state/ThresholdMapper';
import { QueryPart } from 'app/core/components/query_part/query_part';
import alertDef from './state/alertDef';
import config from 'app/core/config';
import appEvents from 'app/core/app_events';
export class AlertTabCtrl {
panel: any;
panelCtrl: any;
testing: boolean;
testResult: any;
subTabIndex: number;
conditionTypes: any;
alert: any;
conditionModels: any;
evalFunctions: any;
evalOperators: any;
noDataModes: any;
executionErrorModes: any;
addNotificationSegment;
notifications;
alertNotifications;
error: string;
appSubUrl: string;
alertHistory: any;
/** @ngInject */
constructor(
private $scope,
private backendSrv,
private dashboardSrv,
private uiSegmentSrv,
private $q,
private 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;
}
$onInit() {
this.addNotificationSegment = this.uiSegmentSrv.newPlusButton();
// subscribe to graph threshold handle changes
const thresholdChangedEventHandler = this.graphThresholdChanged.bind(this);
this.panelCtrl.events.on('threshold-changed', thresholdChangedEventHandler);
// set panel alert edit mode
this.$scope.$on('$destroy', () => {
this.panelCtrl.events.off('threshold-changed', thresholdChangedEventHandler);
this.panelCtrl.editingThresholds = false;
this.panelCtrl.render();
});
// build notification model
this.notifications = [];
this.alertNotifications = [];
this.alertHistory = [];
return this.backendSrv.get('/api/alert-notifications').then(res => {
this.notifications = res;
this.initModel();
this.validateModel();
});
}
getAlertHistory() {
this.backendSrv
.get(`/api/annotations?dashboardId=${this.panelCtrl.dashboard.id}&panelId=${this.panel.id}&limit=50&type=alert`)
.then(res => {
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 {
switch (type) {
case 'email':
return 'fa fa-envelope';
case 'slack':
return 'fa fa-slack';
case 'victorops':
return 'fa fa-pagelines';
case 'webhook':
return 'fa fa-cubes';
case 'pagerduty':
return 'fa fa-bullhorn';
case 'opsgenie':
return 'fa fa-bell';
case 'hipchat':
return 'fa fa-mail-forward';
case 'pushover':
return 'fa fa-mobile';
case 'kafka':
return 'fa fa-random';
case 'teams':
return 'fa fa-windows';
}
return 'fa fa-bell';
}
getNotifications() {
return Promise.resolve(
this.notifications.map(item => {
return this.uiSegmentSrv.newSegment(item.name);
})
);
}
changeTabIndex(newTabIndex) {
this.subTabIndex = newTabIndex;
if (this.subTabIndex === 2) {
this.getAlertHistory();
}
}
notificationAdded() {
const model = _.find(this.notifications, {
name: this.addNotificationSegment.value,
});
if (!model) {
return;
}
this.alertNotifications.push({
name: model.name,
iconClass: this.getNotificationIcon(model.type),
isDefault: false,
});
this.alert.notifications.push({ id: model.id });
// reset plus button
this.addNotificationSegment.value = this.uiSegmentSrv.newPlusButton().value;
this.addNotificationSegment.html = this.uiSegmentSrv.newPlusButton().html;
}
removeNotification(index) {
this.alert.notifications.splice(index, 1);
this.alertNotifications.splice(index, 1);
}
initModel() {
const alert = (this.alert = this.panel.alert);
if (!alert) {
return;
}
alert.conditions = alert.conditions || [];
if (alert.conditions.length === 0) {
alert.conditions.push(this.buildDefaultCondition());
}
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';
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;
},
[]
);
ThresholdMapper.alertToGraphThresholds(this.panel);
for (const addedNotification of alert.notifications) {
const model = _.find(this.notifications, { id: addedNotification.id });
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);
notification.bgColor = '#00678b';
this.alertNotifications.push(notification);
}
}
this.panelCtrl.editingThresholds = true;
this.panelCtrl.render();
}
graphThresholdChanged(evt) {
for (const condition of this.alert.conditions) {
if (condition.type === 'query') {
condition.evaluator.params[evt.handleIndex] = evt.threshold.value;
this.evaluatorParamsChanged();
break;
}
}
}
buildDefaultCondition() {
return {
type: 'query',
query: { params: ['A', '5m', 'now'] },
reducer: { type: 'avg', params: [] },
evaluator: { type: 'gt', params: [null] },
operator: { type: 'and' },
};
}
validateModel() {
if (!this.alert) {
return;
}
let firstTarget;
let foundTarget = null;
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';
}
}
const datasourceName = foundTarget.datasource || this.panel.datasource;
this.datasourceSrv.get(datasourceName).then(ds => {
if (!ds.meta.alerting) {
this.error = 'The datasource does not support alerting queries';
} else if (ds.targetContainsTemplate && ds.targetContainsTemplate(foundTarget)) {
this.error = 'Template variables are not supported in alert queries';
} else {
this.error = '';
}
});
}
}
buildConditionModel(source) {
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, evt) {
switch (evt.name) {
case 'action-remove-part': {
break;
}
case 'get-part-actions': {
return this.$q.when([]);
}
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 this.$q.when(result);
}
}
}
handleReducerPartEvent(conditionModel, evt) {
switch (evt.name) {
case 'action': {
conditionModel.source.reducer.type = evt.action.value;
conditionModel.reducerPart = alertDef.createReducerPart(conditionModel.source.reducer);
break;
}
case 'get-part-actions': {
const result = [];
for (const type of alertDef.reducerTypes) {
if (type.value !== conditionModel.source.reducer.type) {
result.push(type);
}
}
return this.$q.when(result);
}
}
}
addCondition(type) {
const condition = this.buildDefaultCondition();
// add to persited model
this.alert.conditions.push(condition);
// add to view model
this.conditionModels.push(this.buildConditionModel(condition));
}
removeCondition(index) {
this.alert.conditions.splice(index, 1);
this.conditionModels.splice(index, 1);
}
delete() {
appEvents.emit('confirm-modal', {
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: 'fa-trash',
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) {
// 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.emit('confirm-modal', {
title: 'Delete Alert History',
text: 'Are you sure you want to remove all history & annotations for this alert?',
icon: 'fa-trash',
yesText: 'Yes',
onConfirm: () => {
this.backendSrv
.post('/api/annotations/mass-delete', {
dashboardId: this.panelCtrl.dashboard.id,
panelId: this.panel.id,
})
.then(res => {
this.alertHistory = [];
this.panelCtrl.refresh();
});
},
});
}
test() {
this.testing = true;
this.testResult = false;
const payload = {
dashboard: this.dashboardSrv.getCurrent().getSaveModelClone(),
panelId: this.panelCtrl.panel.id,
};
return this.backendSrv.post('/api/alerts/test', payload).then(res => {
this.testResult = res;
this.testing = false;
});
}
}
/** @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);