Added support for allowing alerts to un-ack (#9136)

* Add support for allowing alerts to un-ack

* Updated db_schema.yaml

* Update and rename 263.sql to 265.sql

* Fix up ui a bit.

* Renamed sql file
This commit is contained in:
Neil Lathwood 2018-09-19 23:38:01 +01:00 committed by GitHub
parent 7682093fd0
commit 86d862fb25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 239 additions and 86 deletions

View File

@ -44,6 +44,8 @@ Acknowledge an alert
Route: `/api/v0/alerts/:id`
- id is the alert id, you can obtain a list of alert ids from [`list_alerts`](#function-list_alerts).
- note is the note to add to the alert
- until_clear is a boolean and if set to false, the alert will re-alert if it worsens/betters.
Input:

View File

@ -0,0 +1,34 @@
source: Alerting/Introduction.md
# Introduction
To get started, you first need some alert rules which will react to changes with your devices before raising an alert.
[Creating alert rules](Rules.md)
After that you also need to tell LibreNMS how to notify you when an alert is raised, this is done using `Alert Transports`.
[Configuring alert transports](Transports.md)
The next step is not strictly required but most people find it useful. Creating custom alert templates will help you get
the benefit out of the alert system in general. Whilst we include a default template, it is limited in the data that you
will receive in the alerts.
[Configuring alert templates](Templates.md)
### Managing alerts
When an alert has triggered you will see these in the Alerts -> Notifications page within the Web UI.
This list has a couple of options available to it and we'll explain what these are here.
#### ACK
This column provides you visibility on the status of the alert:
![ack alert](img/ack.png) This alert is currently active and sending alerts. Click this icon to acknowledge the alert.
![unack alert](img/unack.png) This alert is currently acknowledged until the alert clears. Click this icon to un-acknowledge the alert.
![unack alert until fault worsens](img/nunack.png) This alert is currently acknowledged until the alert worsens or gets
better, at which stage it will be automatically unacknowledged and alerts will resume. Click this icon to un-acknowledge the alert.
#### Notes
This column will allow you access to the acknowledge/unacknowledge notes for this alert.
![alert notes](img/notes.png)

BIN
doc/Alerting/img/ack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
doc/Alerting/img/notes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
doc/Alerting/img/nunack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
doc/Alerting/img/unack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -9,6 +9,7 @@ The system requires a set of user-defined rules to evaluate the situation of eac
Table of Content:
- [Introduction](Introduction.md)
- [Rules](Rules.md)
- [Syntax](Rules.md#syntax)
- [Options](Rules.md#options)

View File

@ -12,8 +12,9 @@
* the source code distribution for details.
*/
use LibreNMS\Authentication\LegacyAuth;
use LibreNMS\Alerting\QueryBuilderParser;
use LibreNMS\Authentication\LegacyAuth;
use LibreNMS\Config;
function authToken(\Slim\Route $route)
{
@ -1185,15 +1186,27 @@ function delete_rule()
function ack_alert()
{
check_is_admin();
global $config;
$app = \Slim\Slim::getInstance();
$router = $app->router()->getCurrentRoute()->getParams();
$alert_id = mres($router['id']);
$data = json_decode(file_get_contents('php://input'), true);
if (!is_numeric($alert_id)) {
api_error(400, 'Invalid alert has been provided');
}
if (dbUpdate(array('state' => 2), 'alerts', '`id` = ? LIMIT 1', array($alert_id))) {
$alert = dbFetchRow('SELECT note, info FROM alerts WHERE id=?', [$alert_id]);
$note = $alert['note'];
$info = json_decode($alert['info'], true);
if (!empty($note)) {
$note .= PHP_EOL;
}
$note .= date(Config::get('dateformat.long')) . " - Ack (" . Auth::user()->username . ") {$data['note']}";
$info['until_clear'] = $data['until_clear'];
$info = json_encode($info);
if (dbUpdate(['state' => 2, 'note' => $note, 'info' => $info], 'alerts', '`id` = ? LIMIT 1', [$alert_id])) {
api_success_noresult(200, 'Alert has been acknowledged');
} else {
api_success_noresult(200, 'No Alert by that ID');
@ -1203,16 +1216,25 @@ function ack_alert()
function unmute_alert()
{
check_is_admin();
global $config;
$app = \Slim\Slim::getInstance();
$router = $app->router()->getCurrentRoute()->getParams();
$alert_id = mres($router['id']);
$data = json_decode(file_get_contents('php://input'), true);
if (!is_numeric($alert_id)) {
api_success_noresult(200, 'Alert has been acknowledged');
api_error(400, 'Invalid alert has been provided');
}
if (dbUpdate(array('state' => 1), 'alerts', '`id` = ? LIMIT 1', array($alert_id))) {
$alert = dbFetchRow('SELECT note, info FROM alerts WHERE id=?', [$alert_id]);
$note = $alert['note'];
$info = json_decode($alert['info'], true);
if (!empty($note)) {
$note .= PHP_EOL;
}
$note .= date(Config::get('dateformat.long')) . " - Ack (" . Auth::user()->username . ") {$data['note']}";
if (dbUpdate(['state' => 1, 'note' => $note], 'alerts', '`id` = ? LIMIT 1', [$alert_id])) {
api_success_noresult(200, 'Alert has been unmuted');
} else {
api_success_noresult(200, 'No alert by that ID');

View File

@ -306,36 +306,11 @@ var alerts_grid = $("#alerts_' . $unique_id . '").bootgrid({
alerts_grid.find(".command-ack-alert").on("click", function(e) {
e.preventDefault();
var alert_state = $(this).data("alert_state");
if (alert_state != 2) {
var ack_msg = window.prompt("Enter the reason you are acknowledging this alert:");
} else {
var ack_msg = "";
}
if (typeof ack_msg == "string") {
var alert_id = $(this).data("alert_id");
var state = $(this).data("state");
$.ajax({
type: "POST",
url: "ajax_form.php",
dataType: "json",
data: { type: "ack-alert", alert_id: alert_id, state: state, ack_msg: ack_msg },
success: function (data) {
if (data.status == "ok") {
toastr.success(data.message);
$(".alerts").each(function(index) {
var $sortDictionary = $(this).bootgrid("getSortDictionary");
$(this).reload;
$(this).bootgrid("sort", $sortDictionary);
});
} else {
toastr.error(data.message);
}
},
error: function(){
toastr.error(data.message);
}
});
}
var alert_id = $(this).data(\'alert_id\');
$(\'#ack_alert_id\').val(alert_id);
$(\'#ack_alert_state\').val(alert_state);
$(\'#ack_msg\').val(\'\');
$("#alert_ack_modal").modal(\'show\');
});
alerts_grid.find(".command-alert-note").on("click", function(e) {
e.preventDefault();

View File

@ -28,9 +28,10 @@ use LibreNMS\Config;
header('Content-type: application/json');
$alert_id = $vars['alert_id'];
$state = $vars['state'];
$ack_msg = $vars['ack_msg'];
$alert_id = $vars['alert_id'];
$state = $vars['state'];
$ack_msg = $vars['ack_msg'];
$until_clear = $vars['ack_until_clear'];
$status = 'error';
@ -49,16 +50,31 @@ if (!is_numeric($alert_id)) {
$open = 1;
}
if ($until_clear === 'true') {
$until_clear = true;
} else {
$until_clear = false;
}
$info = json_encode([
'until_clear' => $until_clear,
]);
$username = LegacyAuth::user()->username;
$data = ['state' => $state, 'open' => $open];
$data = [
'state' => $state,
'open' => $open,
'info' => $info,
];
$note = dbFetchCell('SELECT note FROM alerts WHERE id=?', [$alert_id]);
if (!empty($note)) {
$note .= PHP_EOL;
}
$data['note'] = $note . date(Config::get('dateformat.long')) . " - $state_descr ($username) $ack_msg";
if (dbUpdate($data, 'alerts', 'id=?', array($alert_id)) >= 0) {
if ($state === 2) {
if (dbUpdate($data, 'alerts', 'id=?', [$alert_id]) >= 0) {
if (in_array($state, [2, 22])) {
$alert_info = dbFetchRow("SELECT `alert_rules`.`name`,`alerts`.`device_id` FROM `alert_rules` LEFT JOIN `alerts` ON `alerts`.`rule_id` = `alert_rules`.`id` WHERE `alerts`.`id` = ?", [$alert_id]);
log_event("$username acknowledged alert {$alert_info['name']}", $alert_info['device_id'], 'alert', 2, $alert_id);
}

View File

@ -0,0 +1,80 @@
<?php
use LibreNMS\Config;
?>
<form class="form-horizontal">
<div class="modal fade" id="alert_ack_modal" tabindex="-1" role="dialog" aria-labelledby="alert_notes" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h5 class="modal-title" id="alert_notes">Acknowledge Alert</h5>
</div>
<div class="modal-body">
<div class='form-group'>
<label for='ack_msg' class='col-sm-4 col-md-3 control-label' title="Add a message to the acknowledgement">(Un)Acknowledgement note: </label>
<div class="col-sm-8 col-md-9">
<input type='text' id='ack_msg' name='ack_msg' class='form-control' autofocus>
</div>
</div>
<div class="form-group" id="ack_section">
<label for="ack_until_clear" class="col-sm-4 col-md-3 control-label" title="Acknowledge until alert clears">Acknowledge until clear:</label>
<div class="col-sm-8 col-md-9">
<input type='checkbox' name='ack_until_clear' id='ack_until_clear'>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-4 col-md-offset-3 col-sm-3 col-md-2">
<input type="hidden" id="ack_alert_id" name="ack_alert_id" value="">
<input type="hidden" id="ack_alert_state" name="ack_alert_state" value="">
<button class="btn btn-success" id="ack-alert" name="ack-alert">Ack alert</button>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
<script>
$('#alert_ack_modal').on('show.bs.modal', function () {
if ($("#ack_alert_state").val() == 2) {
var button_label = 'Un-acknowledge alert';
$('#ack_section').hide();
} else {
var button_label = 'Acknowledge alert';
$('#ack_section').show();
}
document.getElementById('ack-alert').innerText = button_label;
$("#ack_until_clear").bootstrapSwitch('state', <?php echo Config::get('alert.ack_until_clear') ? 'true' : 'false'; ?>);
});
$("#ack-alert").click('', function(event) {
event.preventDefault();
var ack_alert_id = $("#ack_alert_id").val();
var ack_alert_note = $('#ack_msg').val();
var ack_alert_state = $("#ack_alert_state").val();
var ack_until_clear = $("#ack_until_clear").bootstrapSwitch('state');
$.ajax({
type: "POST",
url: "ajax_form.php",
dataType: "json",
data: { type: "ack-alert", alert_id: ack_alert_id, state: ack_alert_state, ack_msg: ack_alert_note, ack_until_clear: ack_until_clear },
success: function (data) {
if (data.status === "ok") {
toastr.success(data.message);
var $table = $('table.alerts');
var sortDictionary = $table.bootgrid("getSortDictionary");
$table.bootgrid('reload');
$table.bootgrid("sort", sortDictionary);
$("#alert_ack_modal").modal('hide');
} else {
toastr.error(data.message);
}
},
error: function(){
toastr.error(data.message);
}
});
});
</script>

View File

@ -124,25 +124,28 @@ $format = $vars['format'];
foreach (dbFetchRows($sql, $param) as $alert) {
$log = dbFetchCell('SELECT details FROM alert_log WHERE rule_id = ? AND device_id = ? ORDER BY id DESC LIMIT 1', array($alert['rule_id'], $alert['device_id']));
$fault_detail = alert_details($log);
$info = json_decode($alert['info'], true);
$alert_to_ack = '<button type="button" class="btn btn-danger command-ack-alert fa fa-eye" aria-hidden="true" title="Mark as acknowledged" data-target="ack-alert" data-state="' . $alert['state'] . '" data-alert_id="' . $alert['id'] . '" data-alert_state="' . $alert['state'] . '" name="ack-alert"></button>';
$alert_to_nack = '<button type="button" class="btn btn-primary command-ack-alert fa fa-eye-slash" aria-hidden="true" title="Mark as not acknowledged" data-target="ack-alert" data-state="' . $alert['state'] . '" data-alert_id="' . $alert['id'] . '" data-alert_state="' . $alert['state'] . '" name="ack-alert"></button>';
$alert_to_ack = '<button type="button" class="btn btn-danger command-ack-alert fa fa-eye" aria-hidden="true" title="Mark as acknowledged" data-target="ack-alert" data-state="' . $alert['state'] . '" data-alert_id="' . $alert['id'] . '" data-alert_state="' . $alert['state'] . '" name="ack-alert"></button>';
$alert_to_nack = '<button type="button" class="btn btn-primary command-ack-alert fa fa-eye-slash" aria-hidden="true" title="Mark as not acknowledged" data-target="ack-alert" data-state="' . $alert['state'] . '" data-alert_id="' . $alert['id'] . '" data-alert_state="' . $alert['state'] . '" name="ack-alert"></button>';
$alert_to_unack = '<button type="button" class="btn btn-primary command-ack-alert fa fa-eye" aria-hidden="true" title="Mark as not acknowledged" data-target="ack-alert" data-state="' . $alert['state'] . '" data-alert_id="' . $alert['id'] . '" data-alert_state="' . $alert['state'] . '" name="ack-alert"></button>';
$ack_ico = $alert_to_ack;
if ((int)$alert['state'] === 0) {
$ico = '';
$msg = '';
} elseif ((int)$alert['state'] === 1 || (int)$alert['state'] === 3 || (int)$alert['state'] === 4) {
$ico = $alert_to_ack;
if ((int)$alert['state'] === 3) {
$msg = '<i class="fa fa-angle-double-down" style="font-size:20px;" aria-hidden="true" title="Status got worse"></i>';
} elseif ((int)$alert['state'] === 4) {
$msg = '<i class="fa fa-angle-double-up" style="font-size:20px;" aria-hidden="true" title="Status got better"></i>';
}
} elseif ((int)$alert['state'] === 2) {
$ico = $alert_to_nack;
$ack_ico = $alert_to_nack;
if ($info['until_clear'] === false) {
$ack_ico = $alert_to_unack;
} else {
$ack_ico = $alert_to_nack;
}
}
$severity = $alert['severity'];

View File

@ -24,6 +24,7 @@ $page_title = 'Alerts';
<?php
$device['device_id'] = '-1';
require_once 'includes/modal/alert_notes.inc.php';
require_once 'includes/modal/alert_ack.inc.php';
require_once 'includes/common/alerts.inc.php';
echo implode('', $common_output);
unset($device['device_id']);

View File

@ -12,5 +12,6 @@
* the source code distribution for details.
*/
require_once 'includes/modal/alert_notes.inc.php';
require_once 'includes/modal/alert_ack.inc.php';
require_once 'includes/common/alerts.inc.php';
echo implode('', $common_output);

View File

@ -22,6 +22,7 @@ $no_refresh = true;
$default_dash = get_user_pref('dashboard', 0);
require_once 'includes/modal/alert_notes.inc.php';
require_once 'includes/modal/alert_ack.inc.php';
// get all dashboards this user can access and put them into two lists user_dashboards and shared_dashboards
$dashboards = get_dashboards();

View File

@ -329,6 +329,11 @@ $general_conf = array(
'descr' => 'Updates to contact email addresses not honored',
'type' => 'checkbox',
),
[
'name' => 'alert.ack_until_clear',
'descr' => 'Default acknowledge until alert clears option',
'type' => 'checkbox',
]
);
$mail_conf = array(

View File

@ -634,44 +634,51 @@ function RunAcks()
*/
function RunFollowUp()
{
foreach (loadAlerts('alerts.state != 2 && alerts.state > 0 && alerts.open = 0') as $alert) {
$rextra = json_decode($alert['extra'], true);
if ($rextra['invert']) {
continue;
}
if (empty($alert['query'])) {
$alert['query'] = GenSQL($alert['rule'], $alert['builder']);
}
$chk = dbFetchRows($alert['query'], array($alert['device_id']));
//make sure we can json_encode all the datas later
$cnt = count($chk);
for ($i = 0; $i < $cnt; $i++) {
if (isset($chk[$i]['ip'])) {
$chk[$i]['ip'] = inet6_ntop($chk[$i]['ip']);
}
}
$o = sizeof($alert['details']['rule']);
$n = sizeof($chk);
$ret = 'Alert #'.$alert['id'];
$state = 0;
if ($n > $o) {
$ret .= ' Worsens';
$state = 3;
$alert['details']['diff'] = array_diff($chk, $alert['details']['rule']);
} elseif ($n < $o) {
$ret .= ' Betters';
$state = 4;
$alert['details']['diff'] = array_diff($alert['details']['rule'], $chk);
}
if ($state > 0 && $n > 0) {
$alert['details']['rule'] = $chk;
if (dbInsert(array('state' => $state, 'device_id' => $alert['device_id'], 'rule_id' => $alert['rule_id'], 'details' => gzcompress(json_encode($alert['details']), 9)), 'alert_log')) {
dbUpdate(array('state' => $state, 'open' => 1, 'alerted' => 1), 'alerts', 'rule_id = ? && device_id = ?', array($alert['rule_id'], $alert['device_id']));
foreach (loadAlerts('alerts.state > 0 && alerts.open = 0') as $alert) {
if ($alert['state'] != 2 || ($alert['info']['until_clear'] === false)) {
$rextra = json_decode($alert['extra'], true);
if ($rextra['invert']) {
continue;
}
echo $ret.' ('.$o.'/'.$n.")\r\n";
if (empty($alert['query'])) {
$alert['query'] = GenSQL($alert['rule'], $alert['builder']);
}
$chk = dbFetchRows($alert['query'], array($alert['device_id']));
//make sure we can json_encode all the datas later
$cnt = count($chk);
for ($i = 0; $i < $cnt; $i++) {
if (isset($chk[$i]['ip'])) {
$chk[$i]['ip'] = inet6_ntop($chk[$i]['ip']);
}
}
$o = sizeof($alert['details']['rule']);
$n = sizeof($chk);
$ret = 'Alert #' . $alert['id'];
$state = 0;
if ($n > $o) {
$ret .= ' Worsens';
$state = 3;
$alert['details']['diff'] = array_diff($chk, $alert['details']['rule']);
} elseif ($n < $o) {
$ret .= ' Betters';
$state = 4;
$alert['details']['diff'] = array_diff($alert['details']['rule'], $chk);
}
if ($state > 0 && $n > 0) {
$alert['details']['rule'] = $chk;
if (dbInsert(array(
'state' => $state,
'device_id' => $alert['device_id'],
'rule_id' => $alert['rule_id'],
'details' => gzcompress(json_encode($alert['details']), 9)
), 'alert_log')) {
dbUpdate(array('state' => $state, 'open' => 1, 'alerted' => 1), 'alerts', 'rule_id = ? && device_id = ?', array($alert['rule_id'], $alert['device_id']));
}
echo $ret . ' (' . $o . '/' . $n . ")\r\n";
}
}
}//end foreach
}//end RunFollowUp()
@ -679,7 +686,7 @@ function RunFollowUp()
function loadAlerts($where)
{
$alerts = [];
foreach (dbFetchRows("SELECT alerts.id, alerts.device_id, alerts.rule_id, alerts.state, alerts.note FROM alerts WHERE $where") as $alert_status) {
foreach (dbFetchRows("SELECT alerts.id, alerts.device_id, alerts.rule_id, alerts.state, alerts.note, alerts.info FROM alerts WHERE $where") as $alert_status) {
$alert = dbFetchRow(
'SELECT alert_log.id,alert_log.rule_id,alert_log.device_id,alert_log.state,alert_log.details,alert_log.time_logged,alert_rules.rule,alert_rules.severity,alert_rules.extra,alert_rules.name,alert_rules.builder FROM alert_log,alert_rules WHERE alert_log.rule_id = alert_rules.id && alert_log.device_id = ? && alert_log.rule_id = ? && alert_rules.disabled = 0 ORDER BY alert_log.id DESC LIMIT 1',
array($alert_status['device_id'], $alert_status['rule_id'])
@ -696,6 +703,7 @@ function loadAlerts($where)
if (!empty($alert['details'])) {
$alert['details'] = json_decode(gzuncompress($alert['details']), true);
}
$alert['info'] = json_decode($alert_status['info'], true);
$alerts[] = $alert;
}
}
@ -760,7 +768,7 @@ function RunAlerts()
}
}
if ($alert['state'] == 1 && !empty($rextra['count']) && ($rextra['count'] == -1 || $alert['details']['count']++ < $rextra['count'])) {
if (in_array($alert['state'], [1,3,4]) && !empty($rextra['count']) && ($rextra['count'] == -1 || $alert['details']['count']++ < $rextra['count'])) {
if ($alert['details']['count'] < $rextra['count']) {
$noacc = true;
}

View File

@ -29,6 +29,7 @@ alerts:
- { Field: open, Type: int(11), 'Null': false, Extra: '' }
- { Field: note, Type: text, 'Null': true, Extra: '' }
- { Field: timestamp, Type: timestamp, 'Null': false, Extra: 'on update CURRENT_TIMESTAMP', Default: CURRENT_TIMESTAMP }
- { Field: info, Type: text, 'Null': false, Extra: '' }
Indexes:
PRIMARY: { Name: PRIMARY, Columns: [id], Unique: true, Type: BTREE }
unique_alert: { Name: unique_alert, Columns: [device_id, rule_id], Unique: true, Type: BTREE }

View File

@ -104,6 +104,7 @@ pages:
- API/Logs.md
- 8. Alerting:
- Intro: Alerting/index.md
- Alerting/Introduction.md
- Alerting/Rules.md
- Alerting/Templates.md
- Alerting/Transports.md

2
sql-schema/268.sql Normal file
View File

@ -0,0 +1,2 @@
ALTER TABLE `alerts` ADD `info` TEXT NOT NULL;
INSERT INTO `config` (`config_name`, `config_value`, `config_default`, `config_descr`, `config_group`, `config_group_order`, `config_sub_group`, `config_sub_group_order`, `config_hidden`, `config_disabled`) VALUES('alert.ack_until_clear', 'false', 'false', 'Default acknowledge until alert clears', 'alerting', 0, 'general', 0, '0', '0');