Skip to content

Commit 04dbee3

Browse files
authored
[Feature] Added healthy indicator to results (alexjustesen#1814)
* added healthy indicator to results * added benchmark helper to make assessing benchmarks easier * code quality * skip changes to the resource
1 parent b39a692 commit 04dbee3

File tree

6 files changed

+205
-5
lines changed

6 files changed

+205
-5
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace App\Actions\Ookla;
4+
5+
use App\Helpers\Benchmark;
6+
use App\Models\Result;
7+
use Illuminate\Support\Arr;
8+
use Lorisleiva\Actions\Concerns\AsAction;
9+
10+
/**
11+
* TODO: refactored after Sven merges benchmark passed indicator.
12+
*/
13+
class EvaluateResultHealth
14+
{
15+
use AsAction;
16+
17+
public bool $healthy = true;
18+
19+
public function handle(Result $result, array $benchmarks): bool
20+
{
21+
if (Arr::get($benchmarks, 'download', false) && ! Benchmark::bitrate($result->download, $benchmarks['download'])) {
22+
$this->healthy = false;
23+
}
24+
25+
if (Arr::get($benchmarks, 'upload', false) && ! Benchmark::bitrate($result->upload, $benchmarks['upload'])) {
26+
$this->healthy = false;
27+
}
28+
29+
if (Arr::get($benchmarks, 'ping', false) && ! Benchmark::ping($result->ping, $benchmarks['ping'])) {
30+
$this->healthy = false;
31+
}
32+
33+
return $this->healthy;
34+
}
35+
}

app/Helpers/Benchmark.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace App\Helpers;
4+
5+
use Illuminate\Support\Arr;
6+
7+
class Benchmark
8+
{
9+
/**
10+
* Validate if the bitrate passes the benchmark.
11+
*/
12+
public static function bitrate(float|int $bytes, array $benchmark): bool
13+
{
14+
$value = Arr::get($benchmark, 'value');
15+
16+
$unit = Arr::get($benchmark, 'unit');
17+
18+
// Pass the benchmark if the value or unit is empty.
19+
if (blank($value) || blank($unit)) {
20+
return true;
21+
}
22+
23+
return Bitrate::bytesToBits($bytes) < Bitrate::normalizeToBits($value.$unit);
24+
}
25+
26+
/**
27+
* Validate if the ping passes the benchmark.
28+
*/
29+
public static function ping(float|int $ping, array $benchmark): bool
30+
{
31+
$value = Arr::get($benchmark, 'value');
32+
33+
// Pass the benchmark if the value is empty.
34+
if (blank($value)) {
35+
return true;
36+
}
37+
38+
return $ping >= $value;
39+
}
40+
}

app/Helpers/Bitrate.php

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
namespace App\Helpers;
4+
5+
use InvalidArgumentException;
6+
7+
class Bitrate
8+
{
9+
/**
10+
* Units conversion map to bits
11+
* Base unit is bits (not bytes)
12+
*/
13+
private const UNITS = [
14+
'b' => 1,
15+
'kb' => 1000,
16+
'kib' => 1024,
17+
'mb' => 1000000,
18+
'mib' => 1048576,
19+
'gb' => 1000000000,
20+
'gib' => 1073741824,
21+
'tb' => 1000000000000,
22+
'tib' => 1099511627776,
23+
];
24+
25+
/**
26+
* Convert bytes to bits.
27+
*/
28+
public static function bytesToBits(int|float $bytes): int|float
29+
{
30+
if ($bytes < 0) {
31+
throw new InvalidArgumentException('Bytes value cannot be negative');
32+
}
33+
34+
// 1 byte = 8 bits
35+
return $bytes * 8;
36+
}
37+
38+
/**
39+
* Parse and normalize any bit rate to bits.
40+
*/
41+
public static function normalizeToBits(float|int|string $bitrate): float
42+
{
43+
// If numeric, assume it's already in bits
44+
if (is_numeric($bitrate)) {
45+
return (float) $bitrate;
46+
}
47+
48+
// Convert to lowercase and remove any whitespace
49+
$bitrate = strtolower(trim($bitrate));
50+
51+
// Remove 'ps' or 'per second' suffix if present
52+
$bitrate = str_replace(['ps', 'per second'], '', $bitrate);
53+
54+
// Extract numeric value and unit
55+
if (! preg_match('/^([\d.]+)\s*([kmgt]?i?b)$/', $bitrate, $matches)) {
56+
throw new InvalidArgumentException(
57+
"Invalid bitrate format. Expected format: '1.5 Mb', '500kb', etc."
58+
);
59+
}
60+
61+
$value = (float) $matches[1];
62+
$unit = $matches[2];
63+
64+
// Validate unit
65+
if (! isset(self::UNITS[$unit])) {
66+
throw new InvalidArgumentException(
67+
"Invalid unit '$unit'. Supported units: ".implode(', ', array_keys(self::UNITS))
68+
);
69+
}
70+
71+
// Convert to bits
72+
return $value * self::UNITS[$unit];
73+
}
74+
75+
/**
76+
* Format bits to human readable string.
77+
*/
78+
public static function formatBits(float $bits, bool $useBinaryPrefix = false, int $precision = 2): string
79+
{
80+
$units = $useBinaryPrefix
81+
? ['b', 'Kib', 'Mib', 'Gib', 'Tib']
82+
: ['b', 'kb', 'Mb', 'Gb', 'Tb'];
83+
84+
$divisor = $useBinaryPrefix ? 1024 : 1000;
85+
$power = floor(($bits ? log($bits) : 0) / log($divisor));
86+
$power = min($power, count($units) - 1);
87+
88+
return sprintf(
89+
"%.{$precision}f %s",
90+
$bits / pow($divisor, $power),
91+
$units[$power]
92+
);
93+
}
94+
}

app/Jobs/Ookla/BenchmarkSpeedtestJob.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Jobs\Ookla;
44

5+
use App\Actions\Ookla\EvaluateResultHealth;
56
use App\Enums\ResultStatus;
67
use App\Events\SpeedtestBenchmarking;
78
use App\Models\Result;
@@ -45,13 +46,14 @@ public function handle(): void
4546

4647
$benchmarks = $this->buildBenchmarks($settings);
4748

48-
if (count($benchmarks) > 0) {
49-
$this->result->update([
50-
'benchmarks' => $benchmarks,
51-
]);
52-
} else {
49+
if (! count($benchmarks)) {
5350
return;
5451
}
52+
53+
$this->result->update([
54+
'benchmarks' => $benchmarks,
55+
'healthy' => EvaluateResultHealth::run($this->result, $benchmarks),
56+
]);
5557
}
5658

5759
private function buildBenchmarks(ThresholdSettings $settings): array

app/Models/Result.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ protected function casts(): array
3232
return [
3333
'benchmarks' => 'array',
3434
'data' => 'array',
35+
'healthy' => 'boolean',
3536
'service' => ResultService::class,
3637
'status' => ResultStatus::class,
3738
'scheduled' => 'boolean',
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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::table('results', function (Blueprint $table) {
15+
$table->boolean('healthy')
16+
->nullable()
17+
->after('benchmarks');
18+
});
19+
}
20+
21+
/**
22+
* Reverse the migrations.
23+
*/
24+
public function down(): void
25+
{
26+
//
27+
}
28+
};

0 commit comments

Comments
 (0)