feat(wg): WgTraffic — bucketed down/up throughput series
parent
9e6f3775b7
commit
130e1aeaf6
|
|
@ -0,0 +1,70 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\WgTrafficSample;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns per-peer cumulative traffic samples into a bucketed aggregate down/up THROUGHPUT series
|
||||||
|
* for the dashboard SVG chart. Per peer, consecutive samples give a delta; negative deltas
|
||||||
|
* (counter reset on a wg restart) clamp to 0; deltas are summed into time buckets across peers.
|
||||||
|
*/
|
||||||
|
class WgTraffic
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array{window:int, buckets:int, points:array<int,array{t:int,down:int,up:int}>,
|
||||||
|
* total_down:int, total_up:int, peak_down:int, peak_up:int}
|
||||||
|
*/
|
||||||
|
public function series(int $windowSeconds, int $buckets = 60): array
|
||||||
|
{
|
||||||
|
$windowSeconds = max(60, $windowSeconds);
|
||||||
|
$buckets = max(1, $buckets);
|
||||||
|
$now = time();
|
||||||
|
$from = $now - $windowSeconds;
|
||||||
|
$width = $windowSeconds / $buckets;
|
||||||
|
|
||||||
|
$down = array_fill(0, $buckets, 0);
|
||||||
|
$up = array_fill(0, $buckets, 0);
|
||||||
|
|
||||||
|
$rows = WgTrafficSample::query()
|
||||||
|
->where('sampled_at', '>=', now()->subSeconds($windowSeconds + (int) ceil($width) + 5))
|
||||||
|
->orderBy('peer_pubkey')
|
||||||
|
->orderBy('sampled_at')
|
||||||
|
->get(['peer_pubkey', 'rx', 'tx', 'sampled_at']);
|
||||||
|
|
||||||
|
$prev = [];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$pk = $r->peer_pubkey;
|
||||||
|
$t = $r->sampled_at->getTimestamp();
|
||||||
|
if (isset($prev[$pk])) {
|
||||||
|
$dRx = max(0, (int) $r->rx - $prev[$pk]['rx']);
|
||||||
|
$dTx = max(0, (int) $r->tx - $prev[$pk]['tx']);
|
||||||
|
$idx = (int) floor(($t - $from) / $width);
|
||||||
|
if ($idx >= 0 && $idx < $buckets) {
|
||||||
|
$down[$idx] += $dRx;
|
||||||
|
$up[$idx] += (int) $r->tx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$prev[$pk] = ['rx' => (int) $r->rx, 'tx' => (int) $r->tx];
|
||||||
|
}
|
||||||
|
|
||||||
|
$points = [];
|
||||||
|
for ($i = 0; $i < $buckets; $i++) {
|
||||||
|
$points[] = [
|
||||||
|
't' => (int) round($from + ($i + 0.5) * $width),
|
||||||
|
'down' => $down[$i],
|
||||||
|
'up' => $up[$i],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'window' => $windowSeconds,
|
||||||
|
'buckets' => $buckets,
|
||||||
|
'points' => $points,
|
||||||
|
'total_down' => array_sum($down),
|
||||||
|
'total_up' => array_sum($up),
|
||||||
|
'peak_down' => $down === [] ? 0 : max($down),
|
||||||
|
'peak_up' => $up === [] ? 0 : max($up),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\WgTrafficSample;
|
||||||
|
use App\Services\WgTraffic;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class WgTrafficTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
private function sample(string $pk, int $rx, int $tx, int $agoSeconds): void
|
||||||
|
{
|
||||||
|
WgTrafficSample::create([
|
||||||
|
'peer_pubkey' => $pk, 'peer_name' => $pk, 'rx' => $rx, 'tx' => $tx,
|
||||||
|
'sampled_at' => now()->subSeconds($agoSeconds),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_empty_when_no_samples(): void
|
||||||
|
{
|
||||||
|
$s = app(WgTraffic::class)->series(3600, 6);
|
||||||
|
$this->assertSame(0, $s['total_down']);
|
||||||
|
$this->assertSame(0, $s['total_up']);
|
||||||
|
$this->assertCount(6, $s['points']);
|
||||||
|
$this->assertSame(0, array_sum(array_column($s['points'], 'down')));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_aggregates_positive_deltas_across_peers(): void
|
||||||
|
{
|
||||||
|
$this->sample('A', 0, 0, 3000);
|
||||||
|
$this->sample('A', 100, 10, 1800);
|
||||||
|
$this->sample('A', 300, 30, 600);
|
||||||
|
$this->sample('B', 0, 0, 3000);
|
||||||
|
$this->sample('B', 50, 5, 600);
|
||||||
|
|
||||||
|
$s = app(WgTraffic::class)->series(3600, 6);
|
||||||
|
$this->assertSame(350, $s['total_down']);
|
||||||
|
$this->assertSame(45, $s['total_up']);
|
||||||
|
$this->assertGreaterThan(0, $s['peak_down']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_counter_reset_clamps_to_zero(): void
|
||||||
|
{
|
||||||
|
$this->sample('A', 500, 0, 1800);
|
||||||
|
$this->sample('A', 20, 0, 600);
|
||||||
|
|
||||||
|
$s = app(WgTraffic::class)->series(3600, 6);
|
||||||
|
$this->assertSame(0, $s['total_down']);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue