Improved Latency graph (#15940)

* Improved Latency graph
Store loss+jitter info in rrd instead of database
New graph icmp_perf (legacy ping_perf still valid referencing part of the newer data)
Delete device_perf table

* Change loss to an area so it is more visible

* Style fixes

* Cleanups from phpstan & tests

* exit_code fix

* Remove alert usage of device_perf

* Don't use magic __get

* Add test for bulkPing
Add host to previous tests

* style fixes

* Fix issue fping error responses
This commit is contained in:
Tony Murray 2024-04-18 09:57:01 -05:00 committed by GitHub
parent 4cce4f082e
commit 49f8269262
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 466 additions and 495 deletions

View File

@ -31,6 +31,7 @@
namespace LibreNMS\Alert;
use App\Facades\DeviceCache;
use App\Facades\Rrd;
use App\Models\AlertTransport;
use App\Models\Eventlog;
use LibreNMS\Config;
@ -38,6 +39,7 @@ use LibreNMS\Enum\AlertState;
use LibreNMS\Enum\Severity;
use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Polling\ConnectivityHelper;
use LibreNMS\Util\Number;
use LibreNMS\Util\Time;
class RunAlerts
@ -116,13 +118,15 @@ class RunAlerts
$obj['status'] = $device->status;
$obj['status_reason'] = $device->status_reason;
if ((new ConnectivityHelper($device))->canPing()) {
$ping_stats = $device->perf()->latest('timestamp')->first();
$obj['ping_timestamp'] = $ping_stats->timestamp;
$obj['ping_loss'] = $ping_stats->loss;
$obj['ping_min'] = $ping_stats->min;
$obj['ping_max'] = $ping_stats->max;
$obj['ping_avg'] = $ping_stats->avg;
$obj['debug'] = $ping_stats->debug;
$last_ping = Rrd::lastUpdate(Rrd::name($device->hostname, 'icmp-perf'));
if ($last_ping) {
$obj['ping_timestamp'] = $last_ping->timestamp;
$obj['ping_loss'] = Number::calculatePercent($last_ping->get('xmt') - $last_ping->get('rcv'), $last_ping->get('xmt'));
$obj['ping_min'] = $last_ping->get('min');
$obj['ping_max'] = $last_ping->get('max');
$obj['ping_avg'] = $last_ping->get('avg');
$obj['debug'] = 'unsupported';
}
}
$extra = $alert['details'];

View File

@ -26,33 +26,46 @@
namespace LibreNMS\Data\Source;
use LibreNMS\Config;
use LibreNMS\Exceptions\FpingUnparsableLine;
use Log;
use Symfony\Component\Process\Process;
class Fping
{
private string $fping_bin;
private string|false $fping6_bin;
private int $count;
private int $timeout;
private int $interval;
private int $tos;
private int $retries;
public function __construct()
{
// prep fping parameters
$this->fping_bin = Config::get('fping', 'fping');
$fping6 = Config::get('fping6', 'fping6');
$this->fping6_bin = is_executable($fping6) ? $fping6 : false;
$this->count = max(Config::get('fping_options.count', 3), 1);
$this->interval = max(Config::get('fping_options.interval', 500), 20);
$this->timeout = max(Config::get('fping_options.timeout', 500), $this->interval);
$this->retries = Config::get('fping_options.retries', 2);
$this->tos = Config::get('fping_options.tos', 0);
}
/**
* Run fping against a hostname/ip in count mode and collect stats.
*
* @param string $host
* @param int $count (min 1)
* @param int $interval (min 20)
* @param int $timeout (not more than $interval)
* @param string $host hostname or ip
* @param string $address_family ipv4 or ipv6
* @return \LibreNMS\Data\Source\FpingResponse
*/
public function ping($host, $count = 3, $interval = 1000, $timeout = 500, $address_family = 'ipv4'): FpingResponse
public function ping($host, $address_family = 'ipv4'): FpingResponse
{
$interval = max($interval, 20);
$fping = Config::get('fping');
$fping6 = Config::get('fping6');
$fping_tos = Config::get('fping_options.tos', 0);
if ($address_family == 'ipv6') {
$cmd = is_executable($fping6) ? [$fping6] : [$fping, '-6'];
$cmd = $this->fping6_bin === false ? [$this->fping_bin, '-6'] : [$this->fping6_bin];
} else {
$cmd = is_executable($fping6) ? [$fping] : [$fping, '-4'];
$cmd = $this->fping6_bin === false ? [$this->fping_bin, '-4'] : [$this->fping_bin];
}
// build the command
@ -60,13 +73,13 @@ class Fping
'-e',
'-q',
'-c',
max($count, 1),
$this->count,
'-p',
$interval,
$this->interval,
'-t',
max($timeout, $interval),
$this->timeout,
'-O',
$fping_tos,
$this->tos,
$host,
]);
@ -74,10 +87,50 @@ class Fping
Log::debug('[FPING] ' . $process->getCommandLine() . PHP_EOL);
$process->run();
$response = FpingResponse::parseOutput($process->getErrorOutput(), $process->getExitCode());
$response = FpingResponse::parseLine($process->getErrorOutput(), $process->getExitCode());
Log::debug("response: $response");
return $response;
}
public function bulkPing(array $hosts, callable $callback): void
{
$process = app()->make(Process::class, ['command' => [
$this->fping_bin,
'-f', '-',
'-e',
'-t', $this->timeout,
'-r', $this->retries,
'-O', $this->tos,
'-c', $this->count,
]]);
// twice polling interval
$process->setTimeout(Config::get('rrd.step', 300) * 2);
// send hostnames to stdin to avoid overflowing cli length limits
$process->setInput(implode(PHP_EOL, $hosts) . PHP_EOL);
Log::debug('[FPING] ' . $process->getCommandLine() . PHP_EOL);
$partial = '';
$process->run(function ($type, $output) use ($callback, &$partial) {
// stdout contains individual ping responses, stderr contains summaries
if ($type == Process::ERR) {
foreach (explode(PHP_EOL, $output) as $line) {
if ($line) {
Log::debug("Fping OUTPUT|$line PARTIAL|$partial");
try {
$response = FpingResponse::parseLine($partial . $line);
call_user_func($callback, $response);
$partial = '';
} catch (FpingUnparsableLine $e) {
// handle possible partial line
$partial = $e->unparsedLine;
}
}
}
}
});
}
}

View File

@ -25,46 +25,19 @@
namespace LibreNMS\Data\Source;
use App\Models\DevicePerf;
use App\Facades\Rrd;
use App\Models\Device;
use Carbon\Carbon;
use LibreNMS\Exceptions\FpingUnparsableLine;
use LibreNMS\RRD\RrdDefinition;
class FpingResponse
{
/**
* @var int
*/
public $transmitted;
/**
* @var int
*/
public $received;
/**
* @var int
*/
public $loss;
/**
* @var float
*/
public $min_latency;
/**
* @var float
*/
public $max_latency;
/**
* @var float
*/
public $avg_latency;
/**
* @var int
*/
public $duplicates;
/**
* @var int
*/
public $exit_code;
/**
* @var bool
*/
private $skipped;
const SUCESS = 0;
const UNREACHABLE = 1;
const INVALID_HOST = 2;
const INVALID_ARGS = 3;
const SYS_CALL_FAIL = 4;
/**
* @param int $transmitted ICMP packets transmitted
@ -75,23 +48,38 @@ class FpingResponse
* @param float $avg_latency Average latency (ms)
* @param int $duplicates Number of duplicate responses (Indicates network issue)
* @param int $exit_code Return code from fping
* @param string|null $host Hostname/IP pinged
*/
public function __construct(int $transmitted, int $received, int $loss, float $min_latency, float $max_latency, float $avg_latency, int $duplicates, int $exit_code, bool $skipped = false)
private function __construct(
public readonly int $transmitted,
public readonly int $received,
public readonly int $loss,
public readonly float $min_latency,
public readonly float $max_latency,
public readonly float $avg_latency,
public readonly int $duplicates,
public int $exit_code,
public readonly ?string $host = null,
private bool $skipped = false)
{
$this->transmitted = $transmitted;
$this->received = $received;
$this->loss = $loss;
$this->min_latency = $min_latency;
$this->max_latency = $max_latency;
$this->avg_latency = $avg_latency;
$this->duplicates = $duplicates;
$this->exit_code = $exit_code;
$this->skipped = $skipped;
}
public static function artificialUp(): FpingResponse
public static function artificialUp(string $host = null): static
{
return new FpingResponse(1, 1, 0, 0, 0, 0, 0, 0, true);
return new static(1, 1, 0, 0, 0, 0, 0, 0, $host, true);
}
public static function artificialDown(string $host = null): static
{
return new static(1, 0, 100, 0, 0, 0, 0, 0, $host, false);
}
/**
* Change the exit code to 0, this may be approriate when a non-fatal error was encourtered
*/
public function ignoreFailure(): void
{
$this->exit_code = 0;
}
public function wasSkipped(): bool
@ -99,18 +87,24 @@ class FpingResponse
return $this->skipped;
}
public static function parseOutput(string $output, int $code): FpingResponse
public static function parseLine(string $output, int $code = null): FpingResponse
{
preg_match('#= (\d+)/(\d+)/(\d+)%(, min/avg/max = ([\d.]+)/([\d.]+)/([\d.]+))?$#', $output, $parsed);
[, $xmt, $rcv, $loss, , $min, $avg, $max] = array_pad($parsed, 8, 0);
$matched = preg_match('#(\S+)\s*: (xmt/rcv/%loss = (\d+)/(\d+)/(?:(100)%|(\d+)%, min/avg/max = ([\d.]+)/([\d.]+)/([\d.]+))|Name or service not known|Temporary failure in name resolution)$#', $output, $parsed);
if ($loss < 0) {
$xmt = 1;
$rcv = 0;
$loss = 100;
if ($code == 0 && ! $matched) {
throw new FpingUnparsableLine($output);
}
return new FpingResponse(
[, $host, $error, $xmt, $rcv, $loss100, $loss, $min, $avg, $max] = array_pad($parsed, 10, 0);
$loss = $loss100 ?: $loss;
if ($error == 'Name or service not known') {
return new FpingResponse(0, 0, 0, 0, 0, 0, 0, self::INVALID_HOST, $host);
} elseif ($error == 'Temporary failure in name resolution') {
return new FpingResponse(0, 0, 0, 0, 0, 0, 0, self::SYS_CALL_FAIL, $host);
}
return new static(
(int) $xmt,
(int) $rcv,
(int) $loss,
@ -118,7 +112,8 @@ class FpingResponse
(float) $max,
(float) $avg,
substr_count($output, 'duplicate'),
$code
$code ?? ($loss100 ? self::UNREACHABLE : self::SUCESS),
$host,
);
}
@ -131,21 +126,9 @@ class FpingResponse
return $this->exit_code == 0 && $this->loss < 100;
}
public function toModel(): ?DevicePerf
{
return new DevicePerf([
'xmt' => $this->transmitted,
'rcv' => $this->received,
'loss' => $this->loss,
'min' => $this->min_latency,
'max' => $this->max_latency,
'avg' => $this->avg_latency,
]);
}
public function __toString()
{
$str = "xmt/rcv/%loss = $this->transmitted/$this->received/$this->loss%";
$str = "$this->host : xmt/rcv/%loss = $this->transmitted/$this->received/$this->loss%";
if ($this->max_latency) {
$str .= ", min/avg/max = $this->min_latency/$this->avg_latency/$this->max_latency";
@ -153,4 +136,27 @@ class FpingResponse
return $str;
}
public function saveStats(Device $device): void
{
$device->last_ping = Carbon::now();
$device->last_ping_timetaken = $this->avg_latency ?: $device->last_ping_timetaken;
$device->save();
// detailed multi-ping capable graph
app('Datastore')->put($device->toArray(), 'icmp-perf', [
'rrd_def' => RrdDefinition::make()
->addDataset('avg', 'GAUGE', 0, 65535, source_ds: 'ping', source_file: Rrd::name($device->hostname, 'ping-perf'))
->addDataset('xmt', 'GAUGE', 0, 65535)
->addDataset('rcv', 'GAUGE', 0, 65535)
->addDataset('min', 'GAUGE', 0, 65535)
->addDataset('max', 'GAUGE', 0, 65535),
], [
'avg' => $this->avg_latency,
'xmt' => $this->transmitted,
'rcv' => $this->received,
'min' => $this->min_latency,
'max' => $this->max_latency,
]);
}
}

View File

@ -182,6 +182,22 @@ class Rrd extends BaseDatastore
$this->update($rrd, $fields);
}
public function lastUpdate(string $filename): ?TimeSeriesPoint
{
$output = $this->command('lastupdate', $filename, '')[0];
if (preg_match('/((?: \w+)+)\n\n(\d+):((?: [\d.-]+)+)\nOK/', $output, $matches)) {
$data = array_combine(
explode(' ', ltrim($matches[1])),
explode(' ', ltrim($matches[3])),
);
return new TimeSeriesPoint((int) $matches[2], $data);
}
return null;
}
/**
* Updates an rrd database at $filename using $options
* Where $options is an array, each entry which is not a number is replaced with "U"
@ -386,7 +402,7 @@ class Rrd extends BaseDatastore
}
// send the command!
if (in_array($command, ['last', 'list']) && $this->init(false)) {
if (in_array($command, ['last', 'list', 'lastupdate']) && $this->init(false)) {
// send this to our synchronous process so output is guaranteed
$output = $this->sync_process->sendCommand($cmd);
} elseif ($this->init()) {
@ -558,7 +574,7 @@ class Rrd extends BaseDatastore
* @param string $options
* @return string
*
* @throws \LibreNMS\Exceptions\RrdGraphException
* @throws RrdGraphException
*/
public function graph(string $options, array $env = null): string
{

View File

@ -1,6 +1,6 @@
<?php
/**
* tracepath.inc.php
* TimeSeriesPoint.php
*
* -Description-
*
@ -19,25 +19,27 @@
*
* @link https://www.librenms.org
*
* @copyright 2018 Neil Lathwood
* @author Neil Lathwood <neil@lathwood.co.uk>
* @copyright 2024 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
use App\Models\DevicePerf;
namespace LibreNMS\Data\Store;
$perf_info = DevicePerf::where('device_id', $device['device_id'])->latest('timestamp')->first();
if (! empty($perf_info['debug']['traceroute'])) {
echo "
<div class='row'>
<div class='col-md-12'>
<div class='panel panel-default'>
<div class='panel-heading'>
<h3 class='panel-title'>Traceroute ({$perf_info['timestamp']})</h3>
</div>
<div class='panel-body'>
<pre>{$perf_info['debug']['traceroute']}</pre>
</div>
</div>
</div>
</div>";
class TimeSeriesPoint
{
public function __construct(
public readonly int $timestamp,
public readonly array $data,
) {
}
public function ds(): array
{
return array_keys($this->data);
}
public function get(string $name): int|float|null
{
return $this->data[$name] ?? null;
}
}

View File

@ -0,0 +1,34 @@
<?php
/**
* FpingUnparsableLine.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 <https://www.gnu.org/licenses/>.
*
* @link https://www.librenms.org
*
* @copyright 2024 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace LibreNMS\Exceptions;
class FpingUnparsableLine extends \Exception
{
public function __construct(public readonly string $unparsedLine)
{
parent::__construct("Fping unparsable line: $unparsedLine");
}
}

View File

@ -28,12 +28,10 @@ namespace LibreNMS\Polling;
use App\Models\Device;
use App\Models\DeviceOutage;
use App\Models\Eventlog;
use Carbon\Carbon;
use LibreNMS\Config;
use LibreNMS\Data\Source\Fping;
use LibreNMS\Data\Source\FpingResponse;
use LibreNMS\Enum\Severity;
use LibreNMS\RRD\RrdDefinition;
use SnmpQuery;
use Symfony\Component\Process\Process;
@ -98,7 +96,7 @@ class ConnectivityHelper
if ($this->saveMetrics) {
if ($this->canPing()) {
$this->savePingStats($ping_response);
$ping_response->saveStats($this->device);
}
$this->updateAvailability($previous, $this->device->status);
@ -114,20 +112,14 @@ class ConnectivityHelper
public function isPingable(): FpingResponse
{
if (! $this->canPing()) {
return FpingResponse::artificialUp();
return FpingResponse::artificialUp($this->target);
}
$status = app()->make(Fping::class)->ping(
$this->target,
Config::get('fping_options.count', 3),
Config::get('fping_options.interval', 500),
Config::get('fping_options.timeout', 500),
$this->ipFamily()
);
$status = app()->make(Fping::class)->ping($this->target, $this->ipFamily());
if ($status->duplicates > 0) {
Eventlog::log('Duplicate ICMP response detected! This could indicate a network issue.', $this->device, 'icmp', Severity::Warning);
$status->exit_code = 0; // when duplicate is detected fping returns 1. The device is up, but there is another issue. Clue admins in with above event.
$status->ignoreFailure(); // when duplicate is detected fping returns 1. The device is up, but there is another issue. Clue admins in with above event.
}
return $status;
@ -196,26 +188,4 @@ class ConnectivityHelper
$this->device->outages()->save(new DeviceOutage(['going_down' => time()]));
}
}
/**
* Save the ping stats to db and rrd, also updates last_ping_timetaken and saves the device model.
*/
private function savePingStats(FpingResponse $ping_response): void
{
$perf = $ping_response->toModel();
$perf->debug = ['poller_name' => Config::get('distributed_poller_name')];
if (! $ping_response->success() && Config::get('debug.run_trace', false)) {
$perf->debug = array_merge($perf->debug, $this->traceroute());
}
$this->device->perf()->save($perf);
$this->device->last_ping = Carbon::now();
$this->device->last_ping_timetaken = $ping_response->avg_latency ?: $this->device->last_ping_timetaken;
$this->device->save();
app('Datastore')->put($this->device->toArray(), 'ping-perf', [
'rrd_def' => RrdDefinition::make()->addDataset('ping', 'GAUGE', 0, 65535),
], [
'ping' => $ping_response->avg_latency,
]);
}
}

View File

@ -32,6 +32,7 @@ class RrdDefinition
{
private static $types = ['GAUGE', 'DERIVE', 'COUNTER', 'ABSOLUTE', 'DCOUNTER', 'DDERIVE'];
private $dataSets = [];
private $sources = [];
private $skipNameCheck = false;
/**
@ -51,9 +52,11 @@ class RrdDefinition
* @param int $min Minimum allowed value. null means undefined.
* @param int $max Maximum allowed value. null means undefined.
* @param int $heartbeat Heartbeat for this dataset. Uses the global setting if null.
* @param string $source_ds Dataset to copy data from an existing rrd file
* @param string $source_file File to copy data from (may be ommitted copy from the current file)
* @return RrdDefinition
*/
public function addDataset($name, $type, $min = null, $max = null, $heartbeat = null)
public function addDataset($name, $type, $min = null, $max = null, $heartbeat = null, $source_ds = null, $source_file = null)
{
if (empty($name)) {
d_echo('DS must be set to a non-empty string.');
@ -61,7 +64,7 @@ class RrdDefinition
$name = $this->escapeName($name);
$this->dataSets[$name] = [
$name,
$name . $this->createSource($source_ds, $source_file),
$this->checkType($type),
is_null($heartbeat) ? Config::get('rrd.heartbeat') : $heartbeat,
is_null($min) ? 'U' : $min,
@ -78,9 +81,10 @@ class RrdDefinition
*/
public function __toString()
{
return array_reduce($this->dataSets, function ($carry, $ds) {
return $carry . 'DS:' . implode(':', $ds) . ' ';
}, '');
return implode(' ', array_map(fn ($s) => "--source $s ", $this->sources))
. array_reduce($this->dataSets, function ($carry, $ds) {
return $carry . 'DS:' . implode(':', $ds) . ' ';
}, '');
}
/**
@ -107,6 +111,29 @@ class RrdDefinition
return $this;
}
private function createSource(?string $ds, ?string $file): string
{
if (empty($ds)) {
return '';
}
$output = '=' . $ds;
// if is file given, find or add it to the sources list
if ($file) {
$index = array_search($file, $this->sources);
if ($index === false) {
$this->sources[] = $file;
end($this->sources);
$index = key($this->sources);
}
$output .= '[' . ($index + 1) . ']'; // rrdcreate sources are 1 based
}
return $output;
}
/**
* Check that the data set type is valid.
*

View File

@ -27,7 +27,6 @@ namespace App\Http\Controllers\Device\Tabs;
use App\Models\Device;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use LibreNMS\Config;
use LibreNMS\Interfaces\UI\DeviceTab;
use LibreNMS\Util\Smokeping;
@ -37,8 +36,7 @@ class LatencyController implements DeviceTab
{
public function visible(Device $device): bool
{
return Config::get('smokeping.integration') || DB::table('device_perf')
->where('device_id', $device->device_id)->exists();
return Config::get('smokeping.integration') || $device->getAttrib('override_icmp_disable') !== 'true';
}
public function slug(): string
@ -61,15 +59,6 @@ class LatencyController implements DeviceTab
$from = Request::get('dtpickerfrom', Carbon::now(session('preferences.timezone'))->subDays(2)->format(Config::get('dateformat.byminute')));
$to = Request::get('dtpickerto', Carbon::now(session('preferences.timezone'))->format(Config::get('dateformat.byminute')));
$dbfrom = Carbon::createFromFormat(Config::get('dateformat.byminute'), $from)->setTimezone(date_default_timezone_get())->format(Config::get('dateformat.byminute'));
$dbto = Carbon::createFromFormat(Config::get('dateformat.byminute'), $to)->setTimezone(date_default_timezone_get())->format(Config::get('dateformat.byminute'));
$perf = $this->fetchPerfData($device, $dbfrom, $dbto);
$duration = $perf && $perf->isNotEmpty()
? abs(strtotime($perf->first()->date) - strtotime($perf->last()->date)) * 1000
: 0;
$smokeping = new Smokeping($device);
$smokeping_tabs = [];
if ($smokeping->hasInGraph()) {
@ -80,39 +69,10 @@ class LatencyController implements DeviceTab
}
return [
'dtpickerfrom' => $from,
'dtpickerto' => $to,
'duration' => $duration,
'perfdata' => $this->formatPerfData($perf),
'from' => $from,
'to' => $to,
'smokeping' => $smokeping,
'smokeping_tabs' => $smokeping_tabs,
];
}
private function fetchPerfData(Device $device, $from, $to)
{
return DB::table('device_perf')
->where('device_id', $device->device_id)
->whereBetween('timestamp', [$from, $to])
->selectRaw("DATE_FORMAT(IFNULL(CONVERT_TZ(timestamp, @@global.time_zone, ?), timestamp), '%Y-%m-%d %H:%i') date,xmt,rcv,loss,min,max,avg", [session('preferences.timezone')])
->get();
}
/**
* Data ready for json export
*
* @param \Illuminate\Support\Collection $data
* @return array
*/
private function formatPerfData($data)
{
return $data->reduce(function ($data, $entry) {
$data[] = ['x' => $entry->date, 'y' => $entry->loss, 'group' => 0];
$data[] = ['x' => $entry->date, 'y' => $entry->min, 'group' => 1];
$data[] = ['x' => $entry->date, 'y' => $entry->max, 'group' => 2];
$data[] = ['x' => $entry->date, 'y' => $entry->avg, 'group' => 3];
return $data;
}, []);
}
}

View File

@ -231,7 +231,7 @@ class TopDevicesController extends WidgetController
$results = $query->get()->map(function ($device) {
/** @var Device $device */
return $this->standardRow($device, 'device_ping_perf', ['tab' => 'graphs', 'group' => 'poller']);
return $this->standardRow($device, 'device_icmp_perf', ['tab' => 'graphs', 'group' => 'poller']);
});
return $this->formatData('Response time', $results);

View File

@ -26,7 +26,6 @@
namespace App\Jobs;
use App\Models\Device;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@ -34,19 +33,14 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use LibreNMS\Alert\AlertRules;
use LibreNMS\Config;
use LibreNMS\RRD\RrdDefinition;
use LibreNMS\Data\Source\Fping;
use LibreNMS\Data\Source\FpingResponse;
use LibreNMS\Util\Debug;
use Symfony\Component\Process\Process;
class PingCheck implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private $command;
private $wait;
private $rrd_tags;
/** @var \Illuminate\Database\Eloquent\Collection<string, Device>|null List of devices keyed by hostname */
private $devices;
/** @var array List of device group ids to check */
@ -71,20 +65,6 @@ class PingCheck implements ShouldQueue
if (is_array($groups)) {
$this->groups = $groups;
}
// define rrd tags
$rrd_step = Config::get('ping_rrd_step', Config::get('rrd.step', 300));
$rrd_def = RrdDefinition::make()->addDataset('ping', 'GAUGE', 0, 65535, $rrd_step * 2);
$this->rrd_tags = ['rrd_def' => $rrd_def, 'rrd_step' => $rrd_step];
// set up fping process
$timeout = Config::get('fping_options.timeout', 500); // must be smaller than period
$retries = Config::get('fping_options.retries', 2); // how many retries on failure
$tos = Config::get('fping_options.tos', 0); // TOS marking
$fping = Config::get('fping', 'fping'); // use user defined binary
$this->command = [$fping, '-f', '-', '-e', '-t', $timeout, '-r', $retries, '-O', $tos];
$this->wait = Config::get('rrd.step', 300) * 2;
}
/**
@ -98,46 +78,12 @@ class PingCheck implements ShouldQueue
$this->fetchDevices();
$process = new Process($this->command, null, null, null, $this->wait);
d_echo($process->getCommandLine() . PHP_EOL);
// send hostnames to stdin to avoid overflowing cli length limits
$ordered_device_list = $this->tiered->get(1, collect())->keys()// root nodes before standalone nodes
->merge($this->devices->keys())
->unique()
->implode(PHP_EOL);
->unique()->all();
$process->setInput($ordered_device_list);
$process->start(); // start as early as possible
foreach ($process as $type => $line) {
d_echo($line);
if (Process::ERR === $type) {
// Check for devices we couldn't resolve dns for
if (preg_match('/^(?<hostname>[^\s]+): (?:Name or service not known|Temporary failure in name resolution)/', $line, $errored)) {
$this->recordData($errored['hostname'], 'unreachable');
}
continue;
}
if (preg_match_all(
'/^(?<hostname>[^\s]+) is (?<status>alive|unreachable)(?: \((?<rtt>[\d.]+) ms\))?/m',
$line,
$captured
)) {
foreach ($captured[0] as $index => $matched) {
$this->recordData(
$captured['hostname'][$index],
$captured['status'][$index],
$captured['rtt'][$index] ?: 0
);
}
$this->processTier();
}
}
// bulk ping and send FpingResponse's to recordData as they come in
app()->make(Fping::class)->bulkPing($ordered_device_list, [$this, 'handleResponse']);
// check for any left over devices
if ($this->deferred->isNotEmpty()) {
@ -211,8 +157,8 @@ class PingCheck implements ShouldQueue
$this->current = $this->tiered->get($this->current_tier);
// update and remove devices in the current tier
foreach ($this->deferred->pull($this->current_tier, []) as $data) {
$this->recordData(...$data);
foreach ($this->deferred->pull($this->current_tier, []) as $fpingResponse) {
$this->handleResponse($fpingResponse);
}
// try to process the new tier in case we took care of all the devices
@ -223,13 +169,13 @@ class PingCheck implements ShouldQueue
* If the device is on the current tier, record the data and remove it
* $data should have keys: hostname, status, and conditionally rtt
*/
private function recordData(string $hostname, string $status, float $rtt = 0): void
public function handleResponse(FpingResponse $response): void
{
if (Debug::isVerbose()) {
echo "Attempting to record data for $hostname... ";
echo "Attempting to record data for $response->host... ";
}
$device = $this->devices->get($hostname);
$device = $this->devices->get($response->host);
// process the data if this is a standalone device or in the current tier
if ($device->max_depth === 0 || $this->current->has($device->hostname)) {
@ -238,17 +184,15 @@ class PingCheck implements ShouldQueue
}
// mark up only if snmp is not down too
$device->status = ($status == 'alive' && $device->status_reason != 'snmp');
$device->last_ping = Carbon::now();
$device->last_ping_timetaken = $rtt;
$device->status = ($response->success() && $device->status_reason != 'snmp');
if ($device->isDirty('status')) {
// if changed, update reason
$device->status_reason = $device->status ? '' : 'icmp';
$type = $device->status ? 'up' : 'down';
}
$device->save(); // only saves if needed (which is every time because of last_ping)
// save last_ping_timetaken and rrd data
$response->saveStats($device);
if (isset($type)) { // only run alert rules if status changed
echo "Device $device->hostname changed status to $type, running alerts\n";
@ -256,9 +200,6 @@ class PingCheck implements ShouldQueue
$rules->runRules($device->device_id);
}
// add data to rrd
app('Datastore')->put($device->toArray(), 'ping-perf', $this->rrd_tags, ['ping' => $device->last_ping_timetaken]);
// done with this device
$this->complete($device->hostname);
d_echo("Recorded data for $device->hostname (tier $device->max_depth)\n");
@ -267,8 +208,10 @@ class PingCheck implements ShouldQueue
echo "Deferred\n";
}
$this->defer($hostname, $status, $rtt);
$this->defer($response);
}
$this->processTier();
}
/**
@ -285,19 +228,22 @@ class PingCheck implements ShouldQueue
/**
* Defer this data processing until all parent devices are complete
*/
private function defer(string $hostname, string $status, float $rtt): void
private function defer(FpingResponse $response): void
{
$device = $this->devices->get($hostname);
$device = $this->devices->get($response->host);
if ($device == null) {
dd("could not find $response->host");
}
if ($this->deferred->has($device->max_depth)) {
// add this data to the proper tier, unless it already exists...
$tier = $this->deferred->get($device->max_depth);
if (! $tier->has($device->hostname)) {
$tier->put($device->hostname, [$hostname, $status, $rtt]);
$tier->put($device->hostname, $response);
}
} else {
// create a new tier containing this data
$this->deferred->put($device->max_depth, collect([$device->hostname => [$hostname, $status, $rtt]]));
$this->deferred->put($device->max_depth, collect([$device->hostname => $response]));
}
}
}

View File

@ -839,11 +839,6 @@ class Device extends BaseModel
return $this->belongsToMany(self::class, 'device_relationships', 'child_device_id', 'parent_device_id');
}
public function perf(): HasMany
{
return $this->hasMany(\App\Models\DevicePerf::class, 'device_id');
}
public function ports(): HasMany
{
return $this->hasMany(\App\Models\Port::class, 'device_id', 'device_id');

View File

@ -1,57 +0,0 @@
<?php
/**
* DevicePerf.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 <https://www.gnu.org/licenses/>.
*
* @link https://www.librenms.org
*
* @copyright 2018 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace App\Models;
class DevicePerf extends DeviceRelatedModel
{
protected $table = 'device_perf';
protected $fillable = ['device_id', 'timestamp', 'xmt', 'rcv', 'loss', 'min', 'max', 'avg'];
protected $casts = [
'xmt' => 'integer',
'rcv' => 'integer',
'loss' => 'integer',
'min' => 'float',
'max' => 'float',
'avg' => 'float',
'debug' => 'array',
];
public $timestamps = false;
const CREATED_AT = 'timestamp';
protected $attributes = [
'min' => 0,
'max' => 0,
'avg' => 0,
];
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
$model->timestamp = $model->freshTimestamp();
});
}
}

View File

@ -142,7 +142,6 @@ class DeviceObserver
$device->ospfPorts()->delete();
$device->outages()->delete();
$device->packages()->delete();
$device->perf()->delete();
$device->portsFdb()->delete();
$device->portsNac()->delete();
\DB::table('ports_stack')->where('device_id', $device->device_id)->delete();

View File

@ -136,11 +136,6 @@ if ($options['f'] === 'callback') {
\LibreNMS\Util\Stats::submit();
}
if ($options['f'] === 'device_perf') {
$ret = lock_and_purge('device_perf', 'timestamp < DATE_SUB(NOW(),INTERVAL ? DAY)');
exit($ret);
}
if ($options['f'] === 'ports_purge') {
if (Config::get('ports_purge')) {
$lock = Cache::lock('ports_purge', 86000);

View File

@ -381,7 +381,6 @@ main () {
"eventlog"
"authlog"
"callback"
"device_perf"
"purgeusers"
"bill_data"
"alert_log"

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::dropIfExists('device_perf');
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::create('device_perf', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('device_id')->index();
$table->dateTime('timestamp');
$table->integer('xmt');
$table->integer('rcv');
$table->integer('loss');
$table->float('min');
$table->float('max');
$table->float('avg');
$table->text('debug')->nullable();
$table->index(['device_id', 'timestamp']);
});
}
};

View File

@ -66,9 +66,6 @@ been up for 30344 seconds`.
- ping avg (if icmp enabled): `$alert->ping_avg`
- debug (array)
- poller_name - name of poller (for distributed setups)
- If `$config['debug']['run_trace] = true;` is set then this will contain:
- traceroute (if enabled you will receive traceroute output): `$alert->debug['traceroute']`
- traceroute_output (if the traceroute fails this will contain why): `$alert->debug['traceroute_output']`
- Title for the Alert: `$alert->title`
- Time Elapsed, Only available on recovery (`$alert->state == 0`): `$alert->elapsed`
- Rule Builder (the actual rule) (use `{!! $alert->builder !!}`): `$alert->builder`

View File

@ -246,17 +246,6 @@ lnms config:set icmp_check false
If you would like to do this on a per device basis then you can do so
under Device -> Edit -> Misc -> Disable ICMP Test? On
#### traceroute
LibreNMS uses traceroute to record debug information
when a device is down due to icmp AND you have
`lnms config:set debug.run_trace true` set.
!!! setting "external/binaries"
```bash
lnms config:set traceroute /usr/bin/traceroute
```
#### SNMP
SNMP program locations.

View File

@ -902,7 +902,7 @@ function get_graphs(Illuminate\Http\Request $request)
];
$graphs[] = [
'desc' => 'Ping Response',
'name' => 'device_ping_perf',
'name' => 'device_icmp_perf',
];
foreach (dbFetchRows('SELECT * FROM device_graphs WHERE device_id = ? ORDER BY graph', [$device_id]) as $graph) {
$desc = Config::get("graph_types.device.{$graph['graph']}.descr");

View File

@ -0,0 +1,66 @@
<?php
/*
* LibreNMS
*
* Copyright (c) 2014 Neil Lathwood <https://github.com/laf/ http://www.lathwood.co.uk/fa>
*
* 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. Please see LICENSE.txt at the top level of
* the source code distribution for details.
*/
require 'includes/html/graphs/common.inc.php';
$graph_params->scale_min = 0;
if (\LibreNMS\Config::get('applied_site_style') != 'dark') {
// light
$line_color = '#36393d';
$jitter_color = '#ccd2decc';
} else {
// dark
$line_color = '#d1d9eb';
$jitter_color = '#393d45cc';
}
$rrd_filename = Rrd::name($device['hostname'], 'icmp-perf');
$rrd_options .= ' --left-axis-format \'%4.0lfms\' --vertical-label Latency --right-axis 1:0 --right-axis-label \'Loss %\'';
$rrd_options .= ' DEF:ping=' . $rrd_filename . ':avg:AVERAGE';
$rrd_options .= ' DEF:min=' . $rrd_filename . ':min:MIN';
$rrd_options .= ' DEF:max=' . $rrd_filename . ':max:MAX';
$rrd_options .= ' DEF:xmt=' . $rrd_filename . ':xmt:AVERAGE';
$rrd_options .= ' DEF:rcv=' . $rrd_filename . ':rcv:AVERAGE';
$rrd_options .= ' CDEF:top=max,min,-';
$rrd_options .= ' CDEF:loss=xmt,rcv,-,xmt,/,100,*';
// Legend Header
$rrd_options .= " 'COMMENT:Milliseconds Cur Min Max Avg\\n'";
// Min/Max area invisible min line with max (-min) area stacked on top
$rrd_options .= ' LINE:min#00000000:';
$rrd_options .= " AREA:top$jitter_color::STACK";
// Average RTT and legend
$rrd_options .= " LINE2:ping$line_color:RTT";
$rrd_options .= ' GPRINT:ping:LAST:%15.2lf GPRINT:min:LAST:%6.2lf';
$rrd_options .= ' GPRINT:max:LAST:%6.2lf GPRINT:ping:AVERAGE:%6.2lf\\n';
// loss line and legend
$rrd_options .= ' AREA:loss#d42e08:Loss';
$rrd_options .= ' GPRINT:loss:LAST:%14.2lf GPRINT:loss:MIN:%6.2lf';
$rrd_options .= ' GPRINT:loss:MAX:%6.2lf GPRINT:loss:AVERAGE:%6.2lf\\n';
// previous time period before this one
if ($graph_params->visible('previous')) {
$rrd_options .= " COMMENT:' \\n'";
$rrd_options .= " DEF:pingX=$rrd_filename:avg:AVERAGE:start=$prev_from:end=$from";
$rrd_options .= " SHIFT:pingX:$period";
$rrd_options .= " LINE1.25:pingX#CCCCCC:'Prev RTT '";
$rrd_options .= ' GPRINT:pingX:AVERAGE:%6.2lf';
$rrd_options .= " GPRINT:pingX:MAX:%6.2lf 'GPRINT:pingX:AVERAGE:%6.2lf\\n'";
}

View File

@ -12,10 +12,10 @@
* the source code distribution for details.
*/
$filename = Rrd::name($device['hostname'], 'ping-perf');
$filename = Rrd::name($device['hostname'], 'icmp-perf');
$descr = 'Milliseconds';
$ds = 'ping';
$ds = 'avg';
$scale_min = 0;
require 'includes/html/graphs/generic_stats.inc.php';

View File

@ -17,7 +17,6 @@ echo '
require 'includes/html/dev-overview-data.inc.php';
require 'includes/html/dev-groups-overview-data.inc.php';
require 'overview/puppet_agent.inc.php';
require 'overview/tracepath.inc.php';
echo LibreNMS\Plugins::call('device_overview_container', [$device]);
PluginManager::call(DeviceOverviewHook::class, ['device' => DeviceCache::getPrimary()])->each(function ($view) {

View File

@ -1,8 +1,6 @@
<?php
$perf = \DeviceCache::getPrimary()->perf;
if ($perf->isNotEmpty()) {
if (Rrd::checkRrdExists(Rrd::name(DeviceCache::getPrimary()->hostname, 'icmp-perf'))) {
$perf_url = Url('device') . '/device=' . DeviceCache::getPrimary()->device_id . '/tab=graphs/group=poller/';
echo '
<div class="row">
@ -18,7 +16,7 @@ if ($perf->isNotEmpty()) {
$graph = \App\Http\Controllers\Device\Tabs\OverviewController::setGraphWidth([
'device' => DeviceCache::getPrimary()->device_id,
'type' => 'device_ping_perf',
'type' => 'device_icmp_perf',
'from' => \LibreNMS\Config::get('time.day'),
'legend' => 'yes',
'popup_title' => DeviceCache::getPrimary()->hostname . ' - Ping Response',

View File

@ -64,7 +64,7 @@ $menu_options = ['bits' => 'Bits',
'storage' => 'Storage',
'diskio' => 'Disk I/O',
'poller_perf' => 'Poller',
'ping_perf' => 'Ping',
'icmp_perf' => 'Ping',
'temperature' => 'Temperature',
];
$sep = '';

View File

@ -321,10 +321,6 @@ return [
'description' => 'Spezifiziere URL',
'help' => 'Sollte nur gesetzt werden wenn man den Zugriff nur über einen bestimmten Hostnamen/Port erlauben möchte',
],
'device_perf_purge' => [
'description' => 'Entferne Performanzdaten welche älter sind als',
'help' => 'Wird durch daily.sh erledigt',
],
'distributed_poller' => [
'description' => 'aktiviere Distributed Polling (benötigt zusätzliche Konfiguration)',
'help' => 'Aktiviere systemweites Distributed Polling. Dies wird genutzt für Lastverteilung und nicht remote Polling. Lesen Sie hierzu folgende Dokumentation: https://docs.librenms.org/Extensions/Distributed-Poller/',

View File

@ -505,10 +505,6 @@ return [
'description' => 'Specific URL',
'help' => 'This should *only* be set if you want to *force* a particular hostname/port. It will prevent the web interface being usable form any other hostname',
],
'device_perf_purge' => [
'description' => 'Device performance entries older than',
'help' => 'Cleanup done by daily.sh',
],
'discovery_modules' => [
'arp-table' => [
'description' => 'ARP Table',

View File

@ -363,10 +363,6 @@ return [
'description' => 'Journaux de connexions plus anciens que',
'help' => 'Nettoyage effectué par daily.sh',
],
'device_perf_purge' => [
'description' => 'Stats de performances plus anciennes que',
'help' => 'Statistiques de performances des équipements. Le nettoyage effectué par daily.sh',
],
'discovery_modules' => [
'arp-table' => [
'description' => 'Table ARP',

View File

@ -461,10 +461,6 @@ return [
'description' => 'Specific URL',
'help' => 'This should *only* be set if you want to *force* a particular hostname/port. It will prevent the web interface being usable form any other hostname',
],
'device_perf_purge' => [
'description' => 'Device performance entries older than',
'help' => 'Cleanup done by daily.sh',
],
'discovery_modules' => [
'arp-table' => [
'description' => 'ARP Table',

View File

@ -452,10 +452,6 @@ return [
'description' => 'Чітко вказаний URL',
'help' => 'Це налаштування має бути вказане *лише* якщо необхідно *примусити* до використання певного імені хоста та порта. У цьому разі веб інтерфейс буде недоступний з будь-якого іншого імені',
],
'device_perf_purge' => [
'description' => 'Дані про поведінку пристроїв старші за',
'help' => 'Очистка що виконується daily.sh',
],
'discovery_modules' => [
'arp-table' => [
'description' => 'Таблиця ARP',

View File

@ -322,10 +322,6 @@ return [
'description' => '指定 URL',
'help' => 'This should *only* be set if you want to *force* a particular hostname/port. It will prevent the web interface being usable form any other hostname',
],
'device_perf_purge' => [
'description' => '装置效能项目大于',
'help' => 'Cleanup done by daily.sh',
],
'distributed_poller' => [
'description' => '启用分布式轮询 (需要额外设定)',
'help' => 'Enable distributed polling system wide. This is intended for load sharing, not remote polling. You must read the documentation for steps to enable: https://docs.librenms.org/Extensions/Distributed-Poller/',

View File

@ -375,10 +375,6 @@ return [
'description' => '指定 URL',
'help' => 'This should *only* be set if you want to *force* a particular hostname/port. It will prevent the web interface being usable form any other hostname',
],
'device_perf_purge' => [
'description' => '裝置效能項目大於',
'help' => 'Cleanup done by daily.sh',
],
'distributed_poller' => [
'description' => '啟用分散式輪詢 (需要額外設定)',
'help' => 'Enable distributed polling system wide. This is intended for load sharing, not remote polling. You must read the documentation for steps to enable: https://docs.librenms.org/Extensions/Distributed-Poller/',

View File

@ -998,10 +998,6 @@
"default": "H:i:s",
"type": "text"
},
"debug.run_trace": {
"default": false,
"type": "boolean"
},
"default_port_group": {
"default": 0,
"type": "select-dynamic",
@ -6135,4 +6131,4 @@
"type": "text"
}
}
}
}

View File

@ -748,22 +748,6 @@ device_outages:
PRIMARY: { Name: PRIMARY, Columns: [id], Unique: true, 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_outages_device_id_index: { Name: device_outages_device_id_index, Columns: [device_id], Unique: false, Type: BTREE }
device_perf:
Columns:
- { Field: id, Type: 'int unsigned', 'Null': false, Extra: auto_increment }
- { Field: device_id, Type: 'int unsigned', 'Null': false, Extra: '' }
- { Field: timestamp, Type: datetime, 'Null': false, Extra: '' }
- { Field: xmt, Type: int, 'Null': false, Extra: '' }
- { Field: rcv, Type: int, 'Null': false, Extra: '' }
- { Field: loss, Type: int, 'Null': false, Extra: '' }
- { Field: min, Type: 'double(8,2)', 'Null': false, Extra: '' }
- { Field: max, Type: 'double(8,2)', 'Null': false, Extra: '' }
- { Field: avg, Type: 'double(8,2)', 'Null': false, Extra: '' }
- { Field: debug, Type: text, 'Null': true, Extra: '' }
Indexes:
PRIMARY: { Name: PRIMARY, Columns: [id], Unique: true, Type: BTREE }
device_perf_device_id_index: { Name: device_perf_device_id_index, Columns: [device_id], Unique: false, Type: BTREE }
device_perf_device_id_timestamp_index: { Name: device_perf_device_id_timestamp_index, Columns: [device_id, timestamp], Unique: false, Type: BTREE }
device_relationships:
Columns:
- { Field: parent_device_id, Type: 'int unsigned', 'Null': false, Extra: '', Default: '0' }

View File

@ -42,19 +42,21 @@
<div class="form-group">
<label for="dtpickerfrom">{{ __('From') }}</label>
<input type="text" class="form-control" id="dtpickerfrom" name="dtpickerfrom" maxlength="16"
value="{{ $data['dtpickerfrom'] }}" data-date-format="YYYY-MM-DD HH:mm">
value="{{ $data['from'] }}" data-date-format="YYYY-MM-DD HH:mm">
</div>
<div class="form-group">
<label for="dtpickerto">{{ __('To') }}</label>
<input type="text" class="form-control" id="dtpickerto" name="dtpickerto" maxlength=16
value="{{ $data['dtpickerto'] }} " data-date-format="YYYY-MM-DD HH:mm">
value="{{ $data['to'] }} " data-date-format="YYYY-MM-DD HH:mm">
</div>
<input type="submit" class="btn btn-default" id="submit" value="Update">
</form>
</span>
</x-slot>
<div id="performance"></div>
<div id="performance">
<x-graph type="device_icmp_perf" legend="yes" :device="$device" width="600" height="240" :from="$data['from']" :to="$data['to']"></x-graph>
</div>
</x-panel>
@endsection
@ -64,78 +66,6 @@
@push('scripts')
<script type="text/javascript">
var container = document.getElementById('performance');
var names = ['Loss', 'Min latency', 'Max latency', 'Avg latency'];
var groups = new vis.DataSet();
groups.add({
id: 0,
content: names[0],
options: {
drawPoints: {
style: 'circle'
},
shaded: {
orientation: 'bottom'
}
}
});
groups.add({
id: 1,
content: names[1],
options: {
drawPoints: {
style: 'circle'
},
shaded: {
orientation: 'bottom'
}
}
});
groups.add({
id: 2,
content: names[2],
options: {
drawPoints: {
style: 'circle'
},
shaded: {
orientation: 'bottom'
}
}
});
groups.add({
id: 3,
content: names[3],
options: {
drawPoints: {
style: 'circle'
},
shaded: {
orientation: 'bottom'
}
}
});
var items = @json($data['perfdata']);
var dataset = new vis.DataSet(items);
var options = {
barChart: {width: 50, align: 'right'}, // align: left, center, right
drawPoints: false,
legend: {left: {position: "bottom-left"}},
dataAxis: {
icons: true,
showMajorLabels: true,
showMinorLabels: true,
},
zoomMin: 86400, //24hrs
zoomMax: {{ $data['duration'] }},
orientation: 'top'
};
var graph2d = new vis.Graph2d(container, dataset, groups, options);
$(function () {
$("#dtpickerfrom").datetimepicker({
useCurrent: true,

View File

@ -70,7 +70,7 @@ foreach ($files as $file) {
$random = $tmp_path . '/' . mt_rand() . '.xml';
$rrd_file = basename($file, '.rrd');
if ($rrd_file == 'ping-perf') {
if ($rrd_file == 'icmp-perf') {
$step = $icmp_step;
$heartbeat = $icmp_step * 2;
} else {

View File

@ -25,7 +25,9 @@
namespace LibreNMS\Tests;
use LibreNMS\Config;
use LibreNMS\Data\Source\Fping;
use LibreNMS\Data\Source\FpingResponse;
use Symfony\Component\Process\Process;
class FpingTest extends TestCase
@ -49,6 +51,7 @@ class FpingTest extends TestCase
$actual = app()->make(Fping::class)->ping('192.168.1.3');
$this->assertTrue($actual->success());
$this->assertEquals('192.168.1.3', $actual->host);
$this->assertEquals(3, $actual->transmitted);
$this->assertEquals(3, $actual->received);
$this->assertEquals(0, $actual->loss);
@ -67,6 +70,7 @@ class FpingTest extends TestCase
$actual = app()->make(Fping::class)->ping('192.168.1.7');
$this->assertTrue($actual->success());
$this->assertEquals('192.168.1.7', $actual->host);
$this->assertEquals(5, $actual->transmitted);
$this->assertEquals(3, $actual->received);
$this->assertEquals(40, $actual->loss);
@ -85,6 +89,7 @@ class FpingTest extends TestCase
$actual = app()->make(Fping::class)->ping('192.168.53.1');
$this->assertFalse($actual->success());
$this->assertEquals('192.168.53.1', $actual->host);
$this->assertEquals(3, $actual->transmitted);
$this->assertEquals(0, $actual->received);
$this->assertEquals(100, $actual->loss);
@ -108,6 +113,7 @@ OUT;
$actual = app()->make(Fping::class)->ping('192.168.1.2');
$this->assertFalse($actual->success());
$this->assertEquals('192.168.1.2', $actual->host);
$this->assertEquals(3, $actual->transmitted);
$this->assertEquals(3, $actual->received);
$this->assertEquals(0, $actual->loss);
@ -131,4 +137,53 @@ OUT;
return $process;
}
public function testBulkPing()
{
$expected = [
'192.168.1.4' => [3, 3, 0, 0.62, 0.93, 0.71, 0, 0],
'hostname' => [3, 0, 100, 0.0, 0.0, 0.0, 0, 1],
'invalid:characters!' => [0, 0, 0, 0.0, 0.0, 0.0, 0, 2],
'1.1.1.1' => [3, 2, 33, 0.024, 0.054, 0.037, 0, 0],
];
$hosts = array_keys($expected);
$process = \Mockery::mock(Process::class);
$process->shouldReceive('setTimeout')->with(Config::get('rrd.step', 300) * 2);
$process->shouldReceive('setInput')->with(implode("\n", $hosts) . "\n");
$process->shouldReceive('getCommandLine');
$process->shouldReceive('run')->withArgs(function ($callback) {
// simulate incremental output (not always one full line per callback)
call_user_func($callback, Process::ERR, "192.168.1.4 : xmt/rcv/%loss = 3/3/0%, min/avg/max = 0.62/0.71/0.93\nhostname : xmt/rcv/%loss = 3/0/100%");
call_user_func($callback, Process::ERR, "invalid:characters!: Name or service not known\n\n1.1.1.1 : xmt/rcv/%loss = 3/2/33%");
call_user_func($callback, Process::ERR, ", min/avg/max = 0.024/0.037/0.054\n");
return true;
});
$this->app->bind(Process::class, function ($app, $params) use ($process) {
return $process;
});
// make call
$calls = 0;
app()->make(Fping::class)->bulkPing($hosts, function (FpingResponse $response) use ($expected, &$calls) {
$calls++;
$this->assertArrayHasKey($response->host, $expected);
$current = $expected[$response->host];
$this->assertSame($current[0], $response->transmitted);
$this->assertSame($current[1], $response->received);
$this->assertSame($current[2], $response->loss);
$this->assertSame($current[3], $response->min_latency);
$this->assertSame($current[4], $response->max_latency);
$this->assertSame($current[5], $response->avg_latency);
$this->assertSame($current[6], $response->duplicates);
$this->assertSame($current[7], $response->exit_code);
$this->assertFalse($response->wasSkipped());
});
$this->assertEquals(count($expected), $calls);
}
}

View File

@ -20,7 +20,7 @@ class ConnectivityHelperTest extends TestCase
$this->app->singleton(Fping::class, function () {
$mock = Mockery::mock(Fping::class);
$up = FpingResponse::artificialUp();
$down = new FpingResponse(1, 0, 100, 0, 0, 0, 0, 0);
$down = FpingResponse::artificialDown();
$mock->shouldReceive('ping')
->times(8)
->andReturn(