Device Availability Calculation (#11784)

* Device Availability Calculation

* Travis fix

* .

* schema corrections

* flexible duration

* travis

* .

* .

* .

* .

* remove not needed code

* line Text to RRD

* update humantime

* .

* only set up again if device was marked as down

* set RRD area transparency

* save uptime also, to keep last availability as good as possible

* file description correction

* look for outages even if uptime > duration
This commit is contained in:
SourceDoctor 2020-06-22 22:57:30 +02:00 committed by GitHub
parent 7264ab7ff7
commit 4e6a7291a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 490 additions and 17 deletions

View File

@ -0,0 +1,120 @@
<?php
/**
* Availability.php
*
* Availability calculation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2020 Thomas Berberich
* @author Thomas Berberich <sourcehhdoctor@gmail.com>
*/
namespace LibreNMS\Device;
use \App\Models\DeviceOutage;
class Availability
{
/*
* 1 day 1 * 24 * 60 * 60 = 86400
* 1 week 7 * 24 * 60 * 60 = 604800
* 1 month 30 * 24 * 60 * 60 = 2592000
* 1 year 365 * 24 * 60 * 60 = 31536000
*/
public static function day($device, $precision = 3)
{
$duration = 86400;
return self::availability($device, $duration, $precision);
}
public static function week($device, $precision = 3)
{
$duration = 604800;
return self::availability($device, $duration, $precision);
}
public static function month($device, $precision = 3)
{
$duration = 2592000;
return self::availability($device, $duration, $precision);
}
public static function year($device, $precision = 3)
{
$duration = 31536000;
return self::availability($device, $duration, $precision);
}
/**
* Get the availability of this device
*
* @param int $duration timeperiod in seconds
* @param int $precision after comma precision
* @return availability in %
*/
public static function availability($device, $duration, $precision = 3)
{
if (!is_numeric($device['uptime'])) {
return null;
}
$now = time();
$query = DeviceOutage::where('device_id', '=', $device['device_id'])
->where('up_again', '>=', $now - $duration)
->orderBy('going_down');
$found_outages = $query->get();
# no recorded outages found, so use current uptime
if (!count($found_outages)) {
# uptime is greater duration interval -> full availability
if ($device['uptime'] >= $duration) {
return 100 * 1;
} else {
return round(100 * $device['uptime'] / $duration, $precision);
}
}
$oldest_date_going_down = $query->first()->value('going_down');
$oldest_uptime = $query->first()->value('uptime');
$recorded_duration = $now - ($oldest_date_going_down - $oldest_uptime);
if ($recorded_duration > $duration) {
$recorded_duration = $duration;
}
# sum up time period of all outages
$outage_summary = 0;
foreach ($found_outages as $outage) {
# if device is still down, outage goes till $now
$up_again = $outage->up_again ?: $now;
if ($outage->going_down >= ($now - $duration)) {
# outage complete in duration period
$going_down = $outage->going_down;
} else {
# outage partial in duration period, so consider only relevant part
$going_down = $now - $duration;
}
$outage_summary += ($up_again - $going_down);
}
return round(100 * ($recorded_duration - $outage_summary) / $duration, $precision);
}
}

View File

@ -83,4 +83,54 @@ class Time
return $result;
}
/*
* @param integer seconds of a time period
* @return string human readably time period
*/
public static function humanTime($s)
{
$ret = [];
if ($s >= 86400) {
$d = floor($s / 86400);
$s -= $d * 86400;
if ($d == 1) {
$ret[] = $d . " day";
} else {
$ret[] = $d . " days";
}
}
if ($s >= 3600) {
$h = floor($s / 3600);
$s -= $h * 3600;
if ($h == 1) {
$ret[] = $h . " hour";
} else {
$ret[] = $h . " hours";
}
}
if ($s >= 60) {
$m = floor($s / 60);
$s -= $m * 60;
if ($m == 1) {
$ret[] = $m . " minute";
} else {
$ret[] = $m . " minutes";
}
}
if ($s > 0) {
if ($s == 1) {
$ret[] = $s . " second";
} else {
$ret[] = $s . " seconds";
}
}
return implode(' ,', $ret);
}
}

View File

@ -0,0 +1,34 @@
<?php
/**
* Availability.php
*
* -Description-
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2020 Thomas Berberich
* @author Thomas Berberich <sourcehhdoctor@gmail.com>
*/
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Availability extends Model
{
public $timestamps = false;
protected $table = 'availability';
}

View File

@ -0,0 +1,34 @@
<?php
/**
* DeviceOutage.php
*
* -Description-
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2020 Thomas Berberich
* @author Thomas Berberich <sourcehhdoctor@gmail.com>
*/
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class DeviceOutage extends Model
{
public $timestamps = false;
protected $primaryKey = null;
}

View File

@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateDeviceOutagesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('device_outages', function (Blueprint $table) {
$table->unsignedInteger('device_id')->index();
$table->bigInteger('going_down');
$table->bigInteger('up_again')->nullable();
$table->bigInteger('uptime')->nullable();
$table->unique(['device_id', 'going_down']);
});
Schema::create('availability', function (Blueprint $table) {
$table->increments('availability_id');
$table->unsignedInteger('device_id')->index();
$table->bigInteger('duration');
$table->float('availability_perc', 6, 6)->default(0.000000);
$table->unique(['device_id','duration']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('device_outages');
Schema::drop('availability');
}
}

View File

@ -99,6 +99,7 @@ $config['poller_modules']['aruba-controller'] = false;
$config['poller_modules']['entity-physical'] = true;
$config['poller_modules']['entity-state'] = false;
$config['poller_modules']['applications'] = true;
$config['poller_modules']['availability'] = true;
$config['poller_modules']['mib'] = false;
$config['poller_modules']['stp'] = true;
$config['poller_modules']['ntp'] = true;
@ -190,6 +191,8 @@ configured to be ignored by config options.
`applications`: Device application support.
`availability`: Device Availability Calculation.
`cisco-asa-firewall`: Cisco ASA firewall support.
`mib`: Support for generic MIB parsing.

View File

@ -2109,9 +2109,32 @@ function device_is_up($device, $record_perf = false)
if ($response['status']) {
$type = 'up';
$reason = $device['status_reason'];
if ($device['uptime']) {
$going_down = dbFetchCell('SELECT going_down FROM device_outages WHERE device_id=? AND up_again IS NULL', array($device['device_id']));
if (!empty($going_down)) {
$up_again = time() - $device['uptime'];
if ($up_again <= $going_down) {
# network connection loss, not device down
$up_again = time();
}
dbUpdate(
array('device_id' => $device['device_id'], 'up_again' => $up_again),
'device_outages',
'device_id=? and up_again is NULL',
array($device['device_id'])
);
}
}
} else {
$type = 'down';
$reason = $response['status_reason'];
if ($device['uptime']) {
$data = ['device_id' => $device['device_id'],
'uptime' => $device['uptime'],
'going_down' => strtotime($device['last_polled'])];
dbInsert($data, 'device_outages');
}
}
log_event('Device status changed to ' . ucfirst($type) . " from $reason check.", $device, $type);

View File

@ -0,0 +1,25 @@
<?php
$scale_min = '0';
$scale_max = '100';
$float_precision = '3';
$rrd_filename = rrd_name($device['hostname'], array('availability', $vars['duration']));
$ds = 'availability';
$colour_line = '000000';
$colour_area = '8B8BEB44';
$colour_area_max = 'cc9999';
$line_text = \LibreNMS\Util\Time::humanTime($vars['duration']);
$graph_title .= '::'.$line_text;
$graph_max = 1;
$graph_min = 0;
$unit_text = 'Availability(%)';
require 'includes/html/graphs/generic_simplex.inc.php';

View File

@ -54,28 +54,33 @@ unset($sep);
print_optionbar_end();
$graph_enable = $graph_enable[$vars['group']];
$group = $vars['group'];
$graph_enable = $graph_enable[$group];
foreach ($graph_enable as $graph => $entry) {
$graph_array = array();
if ($graph_enable[$graph]) {
if ($graph == 'customoid') {
foreach (dbFetchRows('SELECT * FROM `customoids` WHERE `device_id` = ? ORDER BY `customoid_descr`', array($device['device_id'])) as $graph_entry) {
$graph_title = \LibreNMS\Config::get("graph_types.device.$graph.descr").": ".$graph_entry['customoid_descr'];
$graph_array['type'] = 'customoid_' . $graph_entry['customoid_descr'];
if (!empty($graph_entry['customoid_unit'])) {
$graph_array['unit'] = $graph_entry['customoid_unit'];
} else {
$graph_array['unit'] = 'value';
$metric = basename($vars['metric']);
if (is_file("includes/html/pages/device/graphs/$group.inc.php")) {
include "includes/html/pages/device/graphs/$group.inc.php";
} else {
foreach ($graph_enable as $graph => $entry) {
$graph_array = array();
if ($graph_enable[$graph]) {
if ($graph == 'customoid') {
foreach (dbFetchRows('SELECT * FROM `customoids` WHERE `device_id` = ? ORDER BY `customoid_descr`', array($device['device_id'])) as $graph_entry) {
$graph_title = \LibreNMS\Config::get("graph_types.device.$graph.descr").": ".$graph_entry['customoid_descr'];
$graph_array['type'] = 'customoid_' . $graph_entry['customoid_descr'];
if (!empty($graph_entry['customoid_unit'])) {
$graph_array['unit'] = $graph_entry['customoid_unit'];
} else {
$graph_array['unit'] = 'value';
}
include 'includes/html/print-device-graph.php';
}
} else {
$graph_title = \LibreNMS\Config::get("graph_types.device.$graph.descr");
$graph_array['type'] = 'device_'.$graph;
include 'includes/html/print-device-graph.php';
}
} else {
$graph_title = \LibreNMS\Config::get("graph_types.device.$graph.descr");
$graph_array['type'] = 'device_'.$graph;
include 'includes/html/print-device-graph.php';
}
}
}
$pagetitle[] = 'Graphs';

View File

@ -0,0 +1,31 @@
<?php
$graph_type = 'availability';
$row = 1;
$duration_list = dbFetchRows("SELECT * FROM `availability` WHERE `device_id` = ? ORDER BY `duration`", array($device['device_id']));
foreach ($duration_list as $duration) {
if (is_integer($row / 2)) {
$row_colour = \LibreNMS\Config::get('list_colour.even');
} else {
$row_colour = \LibreNMS\Config::get('list_colour.odd');
}
$graph_array['device'] = $duration['device_id'];
$graph_array['type'] = 'device_'.$graph_type;
$graph_array['duration'] = $duration['duration'];
$human_duration = \LibreNMS\Util\Time::humanTime($duration['duration']);
$graph_title = $device['hostname'].' - '.$human_duration;
echo "<div class='panel panel-default'>
<div class='panel-heading'>
<h3 class='panel-title'>".$human_duration."<div class='pull-right'>".$duration['availability_perc']."%</div></h3>
</div>";
echo "<div class='panel-body'>";
include 'includes/html/print-graphrow.inc.php';
echo "</div></div>";
$row++;
}

View File

@ -0,0 +1,46 @@
<?php
use LibreNMS\RRD\RrdDefinition;
use LibreNMS\Config;
if (isset($device['uptime']) && ($device['uptime'] > 0 )) {
$graphs['availability'] = true;
$col = dbFetchColumn('SELECT duration FROM availability WHERE device_id = ?', array($device['device_id']));
foreach (Config::get('graphing.availability') as $duration) {
if (!in_array($duration, $col)) {
$data = ['device_id' => $device['device_id'],
'duration' => $duration];
dbInsert($data, 'availability');
}
}
echo 'Availability: ' . PHP_EOL;
foreach (dbFetchRows('SELECT * FROM availability WHERE device_id = ?', array($device['device_id'])) as $row) {
//delete not more interested availabilities
if (!in_array($row['duration'], Config::get('graphing.availability'))) {
dbDelete('availability', 'availability_id=?', array($row['availability_id']));
continue;
}
$avail = \LibreNMS\Device\Availability::availability($device, $row['duration']);
$human_time = \LibreNMS\Util\Time::humanTime($row['duration']);
$rrd_name = array('availability', $row['duration']);
$rrd_def = RrdDefinition::make()
->addDataset('availability', 'GAUGE', 0);
$fields = array(
'availability' => $avail,
);
$tags = array('name' => $row['duration'], 'rrd_def' => $rrd_def, 'rrd_name' => $rrd_name);
data_update($device, 'availability', $tags, $fields);
dbUpdate(array('availability_perc' => $avail), 'availability', '`availability_id` = ?', array($row['availability_id']));
echo $human_time . ' : ' . $avail . '%'. PHP_EOL;
}
unset($duration);
}

View File

@ -612,6 +612,18 @@
"/-K9W8-/"
]
},
"graphing.availability": {
"group": "graphing",
"section": "availability",
"order": 0,
"default": [
"86400",
"604800",
"2592000",
"31536000"
],
"type": "array"
},
"bad_disk_regexp": {
"default": [],
"type": "array"
@ -2783,6 +2795,14 @@
},
"type": "graph"
},
"graph_types.device.availability": {
"default": {
"section": "availability",
"order": 0,
"descr": "Device Availability"
},
"type": "graph"
},
"graph_types.device.sub10_sub10RadioLclAFER": {
"default": {
"section": "wireless",
@ -4013,6 +4033,13 @@
"default": false,
"type": "boolean"
},
"poller_modules.availability": {
"order": 25,
"group": "poller",
"section": "poller_modules",
"default": true,
"type": "boolean"
},
"poller_modules.os": {
"order": 320,
"group": "poller",

View File

@ -189,6 +189,17 @@ authlog:
- { Field: result, Type: text, 'Null': false, Extra: '' }
Indexes:
PRIMARY: { Name: PRIMARY, Columns: [id], Unique: true, Type: BTREE }
availability:
Columns:
- { Field: availability_id, Type: 'int unsigned', 'Null': false, Extra: auto_increment }
- { Field: device_id, Type: 'int unsigned', 'Null': false, Extra: '' }
- { Field: duration, Type: bigint, 'Null': false, Extra: '' }
- { Field: availability_perc, Type: 'double(6,6)', 'Null': false, Extra: '', Default: '0.000000' }
Indexes:
PRIMARY: { Name: PRIMARY, Columns: [availability_id], Unique: true, Type: BTREE }
availability_device_id_index: { Name: availability_device_id_index, Columns: [device_id], Unique: false, Type: BTREE }
availability_device_id_duration_unique: { Name: availability_device_id_duration_unique, Columns: [device_id, duration], Unique: true, Type: BTREE }
bgpPeers:
Columns:
- { Field: bgpPeer_id, Type: 'int unsigned', 'Null': false, Extra: auto_increment }
@ -559,6 +570,15 @@ device_groups:
Indexes:
PRIMARY: { Name: PRIMARY, Columns: [id], Unique: true, Type: BTREE }
device_groups_name_unique: { Name: device_groups_name_unique, Columns: [name], Unique: true, Type: BTREE }
device_outages:
Columns:
- { Field: device_id, Type: 'int unsigned', 'Null': false, Extra: '' }
- { Field: going_down, Type: bigint, 'Null': false, Extra: '' }
- { Field: up_again, Type: bigint, 'Null': true, Extra: '' }
- { Field: uptime, Type: bigint, 'Null': true, Extra: '' }
Indexes:
device_outages_device_id_index: { Name: device_outages_device_id_index, Columns: [device_id], Unique: false, Type: BTREE }
device_outages_device_id_going_down_unique: { Name: device_outages_device_id_going_down_unique, Columns: [device_id, going_down], Unique: true, Type: BTREE }
device_group_device:
Columns:
- { Field: device_group_id, Type: 'int unsigned', 'Null': false, Extra: '' }

View File

@ -11,6 +11,7 @@ return [
'global' => 'Global',
'os' => 'OS',
'discovery' => 'Discovery',
'graphing' => 'Graphing',
'poller' => 'Poller',
'system' => 'System',
'webui' => 'Web UI',
@ -45,6 +46,9 @@ return [
'unix-agent' => 'Unix-Agent Integration',
'smokeping' => 'Smokeping Integration'
],
'graphing' => [
'availability' => 'Device Availability',
],
'poller' => [
'distributed' => 'Distributed Poller',
'graphite' => 'Datastore: Graphite',
@ -636,6 +640,12 @@ return [
'help' => 'Will add the prefix to the start of all metrics. Must be alphanumeric separated by dots'
]
],
'graphing' => [
'availability' => [
'description' => 'Duration',
'help' => 'Calculate Device Availability for listed durations. (Durations are defined in seconds)'
],
],
'graylog' => [
'base_uri' => [
'description' => 'Base URI',
@ -1001,6 +1011,9 @@ return [
'aruba-controller' => [
'description' => 'Aruba Controller'
],
'availability' => [
'description' => 'Availability'
],
'entity-physical' => [
'description' => 'Entity Physical'
],