diff --git a/app/Services/WgTraffic.php b/app/Services/WgTraffic.php new file mode 100644 index 0000000..ce3f6d1 --- /dev/null +++ b/app/Services/WgTraffic.php @@ -0,0 +1,70 @@ +, + * 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), + ]; + } +} diff --git a/tests/Feature/WgTrafficTest.php b/tests/Feature/WgTrafficTest.php new file mode 100644 index 0000000..983f221 --- /dev/null +++ b/tests/Feature/WgTrafficTest.php @@ -0,0 +1,53 @@ + $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']); + } +}