Skip to content

Commit e59be9a

Browse files
eskyuumurrant
andauthored
Add device ICMP statistics for alerting (librenms#17041)
* Add packet loss column to the devices table * Local fix to allow me to roll bac schema * Create a new device_stats table to store dynamic stats for devices. Initial data is some RTT and packet loss stats/ * Formatting fix * Update DB schema * Remove unneeded migration * Remove refernce to device loss column * Added some documentation around the averaging factor option * Update to use LibrenmsConfig * Add drop if exists on schema migration * Remove duplicate code * Include the device ID when creating a new devicestats object * Allow nulls in stats columns * Refactor DeviceStats to extend DeviceRelatedModel Updated DeviceStats to extend DeviceRelatedModel and removed the device relationship method. * Remove unused BelongsTo import in DeviceStats * Change ping RTT and loss fields to nullable * Added the device_stats to the alert entities documentat --------- Co-authored-by: Tony Murray <[email protected]>
1 parent 5c4ea6d commit e59be9a

File tree

9 files changed

+159
-0
lines changed

9 files changed

+159
-0
lines changed

LibreNMS/Data/Source/FpingResponse.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use App\Facades\LibrenmsConfig;
3030
use App\Facades\Rrd;
3131
use App\Models\Device;
32+
use App\Models\DeviceStats;
3233
use Carbon\Carbon;
3334
use LibreNMS\Exceptions\FpingUnparsableLine;
3435
use LibreNMS\RRD\RrdDefinition;
@@ -141,6 +142,25 @@ public function __toString(): string
141142

142143
public function saveStats(Device $device): void
143144
{
145+
$stats = $device->stats ?? new DeviceStats(['device_id' => $device->device_id]);
146+
$stats->ping_last_timestamp = Carbon::now();
147+
// Only update the latency if we have data
148+
if ($this->avg_latency) {
149+
$stats->ping_rtt_prev = $stats->ping_rtt_last ?: $this->avg_latency;
150+
$stats->ping_rtt_last = $this->avg_latency;
151+
// Average is calcualted as the exponential weighted moving average
152+
$stats->ping_rtt_avg = $stats->ping_rtt_avg ? $stats->ping_rtt_avg + (($stats->ping_rtt_last - $stats->ping_rtt_avg) * LibrenmsConfig::get('device_stats_avg_factor')) : $stats->ping_rtt_last;
153+
}
154+
// Only update loss if we transmitted a packet
155+
if ($this->transmitted) {
156+
$stats->ping_loss_prev = $stats->ping_loss_last ?: 100 * ($this->transmitted - $this->received) / $this->transmitted;
157+
$stats->ping_loss_last = 100 * ($this->transmitted - $this->received) / $this->transmitted;
158+
// Average is calcualted as the exponential weighted moving average
159+
$stats->ping_loss_avg = $stats->ping_loss_avg ? $stats->ping_loss_avg + (($stats->ping_loss_last - $stats->ping_loss_avg) * LibrenmsConfig::get('device_stats_avg_factor')) : $stats->ping_loss_last;
160+
}
161+
$stats->save();
162+
163+
// Update the stats stored in the device table until the field is removed
144164
$device->last_ping = Carbon::now();
145165
$device->last_ping_timetaken = $this->avg_latency ?: $device->last_ping_timetaken;
146166
$device->save();

app/Models/Device.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1180,6 +1180,14 @@ public function services(): HasMany
11801180
return $this->hasMany(Service::class, 'device_id');
11811181
}
11821182

1183+
/**
1184+
* @return \Illuminate\Database\Eloquent\Relations\HasOne<\App\Models\DeviceStats, $this>
1185+
*/
1186+
public function stats(): HasOne
1187+
{
1188+
return $this->hasOne(DeviceStats::class, 'device_id');
1189+
}
1190+
11831191
/**
11841192
* @return \Illuminate\Database\Eloquent\Relations\HasMany<\App\Models\Storage, $this>
11851193
*/

app/Models/DeviceStats.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace App\Models;
4+
5+
use Illuminate\Database\Eloquent\Factories\HasFactory;
6+
7+
class DeviceStats extends DeviceRelatedModel
8+
{
9+
use HasFactory;
10+
11+
protected $fillable = [
12+
'device_id',
13+
'ping_last_timestamp',
14+
'ping_rtt_last',
15+
'ping_rtt_prev',
16+
'ping_rtt_avg',
17+
'ping_loss_last',
18+
'ping_loss_prev',
19+
'ping_loss_avg',
20+
];
21+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*/
12+
public function up(): void
13+
{
14+
Schema::dropIfExists('device_stats');
15+
Schema::create('device_stats', function (Blueprint $table) {
16+
$table->id();
17+
$table->timestamps();
18+
$table->unsignedInteger('device_id')->unique();
19+
$table->timestamp('ping_last_timestamp');
20+
$table->float('ping_rtt_last')->unsigned()->nullable();
21+
$table->float('ping_rtt_prev')->unsigned()->nullable();
22+
$table->float('ping_rtt_avg')->unsigned()->nullable();
23+
$table->float('ping_rtt_diff_avg_last')->virtualAs('ping_rtt_last - ping_rtt_avg');
24+
$table->float('ping_rtt_diff_prev_last')->virtualAs('ping_rtt_last - ping_rtt_prev');
25+
$table->float('ping_loss_last')->unsigned()->nullable();
26+
$table->float('ping_loss_prev')->unsigned()->nullable();
27+
$table->float('ping_loss_avg')->unsigned()->nullable();
28+
$table->foreign('device_id')->references('device_id')->on('devices')->onDelete('cascade');
29+
});
30+
}
31+
32+
/**
33+
* Reverse the migrations.
34+
*/
35+
public function down(): void
36+
{
37+
Schema::dropIfExists('device_stats');
38+
}
39+
};

doc/Alerting/Entities.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,21 @@ Entity | Description
2525
`devices.last_polled` | The the last polled datetime (yyyy-mm-dd hh:mm:ss)
2626
`devices.type` | The device type such as network, server, firewall, etc.
2727

28+
## Device Stats
29+
30+
Entity | Description
31+
---|---
32+
`device_stats.ping_loss_last` | Packet loss at last poll (percent)
33+
`device_stats.ping_loss_avg` | Average packet loss (percent)
34+
`device_stats.ping_rtt_last` | Ping RTT at last poll (ms)
35+
`device_stats.ping_rtt_avg` | Average ping RTT (ms)
36+
`device_stats.ping_rtt_diff_avg_last` | Difference between the RTT last and average ping (ms)
37+
`device_stats.ping_rtt_diff_avg_last` | Difference between the RTT of the ping from the last 2 polls (ms)
38+
39+
Details on how the averages above are calculated can be found [here](../Support/Configuration.md#averaging-factor).
40+
41+
The difference fields can be used to detect when ping times increase from their normal valuse.
42+
2843
## BGP Peers
2944

3045
Entity | Description

doc/Support/Configuration.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,6 +1156,28 @@ is discovered.
11561156
lnms config:set storage_perc_warn 60
11571157
```
11581158
1159+
## Averaging Factor
1160+
1161+
LibreNMS keeps track of average values in the database for some metrics so we
1162+
can alert on changes (e.g. if the ping time increases from the average). To
1163+
achieve this goal we want the average to move slowly when there is a change
1164+
to the values being recorded so there is time to alert, but we also need to
1165+
eventually stop alerting if the average value becomes the new normal.
1166+
1167+
The following configuration variable can be adjusted if you make use of the
1168+
average values, and find that they either change too quickly or slowly. If
1169+
you make this setting bigger (closer to 1) the averages will change faster,
1170+
and if you make it smaller (closer to 0) the average will change slower.
1171+
1172+
```bash
1173+
lnms config:set device_stats_avg_factor 0.05
1174+
```
1175+
1176+
If you want to understand more about this, the device statistics uses an
1177+
exponential weighted moving average function to update the average without
1178+
needing to keep multiple values. You can look up independently if you want
1179+
to understand more about this option.
1180+
11591181
## IRC Bot
11601182
11611183
Please refer to [IRC Bot](../Extensions/IRC-Bot.md)

lang/en/settings.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2364,6 +2364,10 @@
23642364
'description' => 'Show devices dependecies on location map',
23652365
'help' => 'Show links between devices on the location map based on parent dependencies',
23662366
],
2367+
'device_stats_avg_factor' => [
2368+
'description' => 'Averaging factor',
2369+
'help' => 'We calculate a moving average using an exponential weighted moving average function. This is the factor used by the function to control how much the current value affects the average. Values closer to 1 will make the average change quicker.',
2370+
],
23672371
'whois' => [
23682372
'description' => 'Path to whois',
23692373
],

resources/definitions/config_definitions.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1447,6 +1447,16 @@
14471447
"{{ $ip }}": "ip"
14481448
}
14491449
},
1450+
"device_stats_avg_factor": {
1451+
"default": 0.05,
1452+
"group": "poller",
1453+
"section": "ping",
1454+
"order": 4,
1455+
"type": "float",
1456+
"validate": {
1457+
"value": "numeric|between:0.000001,0.9"
1458+
}
1459+
},
14501460
"device_traffic_descr": {
14511461
"default": [
14521462
"/loopback/",

resources/definitions/schema/db_schema.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,26 @@ device_relationships:
733733
Constraints:
734734
device_relationship_child_device_id_fk: { name: device_relationship_child_device_id_fk, foreign_key: child_device_id, table: devices, key: device_id, extra: 'ON DELETE CASCADE' }
735735
device_relationship_parent_device_id_fk: { name: device_relationship_parent_device_id_fk, foreign_key: parent_device_id, table: devices, key: device_id, extra: 'ON DELETE CASCADE' }
736+
device_stats:
737+
Columns:
738+
- { Field: id, Type: 'bigint unsigned', 'Null': false, Extra: auto_increment }
739+
- { Field: created_at, Type: timestamp, 'Null': true, Extra: '' }
740+
- { Field: updated_at, Type: timestamp, 'Null': true, Extra: '' }
741+
- { Field: device_id, Type: 'int unsigned', 'Null': false, Extra: '' }
742+
- { Field: ping_last_timestamp, Type: timestamp, 'Null': false, Extra: '' }
743+
- { Field: ping_rtt_last, Type: 'double unsigned', 'Null': true, Extra: '' }
744+
- { Field: ping_rtt_prev, Type: 'double unsigned', 'Null': true, Extra: '' }
745+
- { Field: ping_rtt_avg, Type: 'double unsigned', 'Null': true, Extra: '' }
746+
- { Field: ping_rtt_diff_avg_last, Type: double, 'Null': true, Extra: 'VIRTUAL GENERATED' }
747+
- { Field: ping_rtt_diff_prev_last, Type: double, 'Null': true, Extra: 'VIRTUAL GENERATED' }
748+
- { Field: ping_loss_last, Type: 'double unsigned', 'Null': true, Extra: '' }
749+
- { Field: ping_loss_prev, Type: 'double unsigned', 'Null': true, Extra: '' }
750+
- { Field: ping_loss_avg, Type: 'double unsigned', 'Null': true, Extra: '' }
751+
Indexes:
752+
PRIMARY: { Name: PRIMARY, Columns: [id], Unique: true, Type: BTREE }
753+
device_stats_device_id_unique: { Name: device_stats_device_id_unique, Columns: [device_id], Unique: true, Type: BTREE }
754+
Constraints:
755+
device_stats_device_id_foreign: { name: device_stats_device_id_foreign, foreign_key: device_id, table: devices, key: device_id, extra: 'ON DELETE CASCADE' }
736756
entityState:
737757
Columns:
738758
- { Field: entity_state_id, Type: 'int unsigned', 'Null': false, Extra: auto_increment }

0 commit comments

Comments
 (0)