Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Added healthy indicator to results #1814

Merged
merged 4 commits into from
Nov 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions app/Actions/Ookla/EvaluateResultHealth.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace App\Actions\Ookla;

use App\Helpers\Benchmark;
use App\Models\Result;
use Illuminate\Support\Arr;
use Lorisleiva\Actions\Concerns\AsAction;

/**
* TODO: refactored after Sven merges benchmark passed indicator.
*/
class EvaluateResultHealth
{
use AsAction;

public bool $healthy = true;

public function handle(Result $result, array $benchmarks): bool
{
if (Arr::get($benchmarks, 'download', false) && ! Benchmark::bitrate($result->download, $benchmarks['download'])) {
$this->healthy = false;
}

if (Arr::get($benchmarks, 'upload', false) && ! Benchmark::bitrate($result->upload, $benchmarks['upload'])) {
$this->healthy = false;
}

if (Arr::get($benchmarks, 'ping', false) && ! Benchmark::ping($result->ping, $benchmarks['ping'])) {
$this->healthy = false;
}

return $this->healthy;
}
}
40 changes: 40 additions & 0 deletions app/Helpers/Benchmark.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace App\Helpers;

use Illuminate\Support\Arr;

class Benchmark
{
/**
* Validate if the bitrate passes the benchmark.
*/
public static function bitrate(float|int $bytes, array $benchmark): bool
{
$value = Arr::get($benchmark, 'value');

$unit = Arr::get($benchmark, 'unit');

// Pass the benchmark if the value or unit is empty.
if (blank($value) || blank($unit)) {
return true;
}

return Bitrate::bytesToBits($bytes) < Bitrate::normalizeToBits($value.$unit);
}

/**
* Validate if the ping passes the benchmark.
*/
public static function ping(float|int $ping, array $benchmark): bool
{
$value = Arr::get($benchmark, 'value');

// Pass the benchmark if the value is empty.
if (blank($value)) {
return true;
}

return $ping >= $value;
}
}
94 changes: 94 additions & 0 deletions app/Helpers/Bitrate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

namespace App\Helpers;

use InvalidArgumentException;

class Bitrate
{
/**
* Units conversion map to bits
* Base unit is bits (not bytes)
*/
private const UNITS = [
'b' => 1,
'kb' => 1000,
'kib' => 1024,
'mb' => 1000000,
'mib' => 1048576,
'gb' => 1000000000,
'gib' => 1073741824,
'tb' => 1000000000000,
'tib' => 1099511627776,
];

/**
* Convert bytes to bits.
*/
public static function bytesToBits(int|float $bytes): int|float
{
if ($bytes < 0) {
throw new InvalidArgumentException('Bytes value cannot be negative');
}

// 1 byte = 8 bits
return $bytes * 8;
}

/**
* Parse and normalize any bit rate to bits.
*/
public static function normalizeToBits(float|int|string $bitrate): float
{
// If numeric, assume it's already in bits
if (is_numeric($bitrate)) {
return (float) $bitrate;
}

// Convert to lowercase and remove any whitespace
$bitrate = strtolower(trim($bitrate));

// Remove 'ps' or 'per second' suffix if present
$bitrate = str_replace(['ps', 'per second'], '', $bitrate);

// Extract numeric value and unit
if (! preg_match('/^([\d.]+)\s*([kmgt]?i?b)$/', $bitrate, $matches)) {
throw new InvalidArgumentException(
"Invalid bitrate format. Expected format: '1.5 Mb', '500kb', etc."
);
}

$value = (float) $matches[1];
$unit = $matches[2];

// Validate unit
if (! isset(self::UNITS[$unit])) {
throw new InvalidArgumentException(
"Invalid unit '$unit'. Supported units: ".implode(', ', array_keys(self::UNITS))
);
}

// Convert to bits
return $value * self::UNITS[$unit];
}

/**
* Format bits to human readable string.
*/
public static function formatBits(float $bits, bool $useBinaryPrefix = false, int $precision = 2): string
{
$units = $useBinaryPrefix
? ['b', 'Kib', 'Mib', 'Gib', 'Tib']
: ['b', 'kb', 'Mb', 'Gb', 'Tb'];

$divisor = $useBinaryPrefix ? 1024 : 1000;
$power = floor(($bits ? log($bits) : 0) / log($divisor));
$power = min($power, count($units) - 1);

return sprintf(
"%.{$precision}f %s",
$bits / pow($divisor, $power),
$units[$power]
);
}
}
12 changes: 7 additions & 5 deletions app/Jobs/Ookla/BenchmarkSpeedtestJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Jobs\Ookla;

use App\Actions\Ookla\EvaluateResultHealth;
use App\Enums\ResultStatus;
use App\Events\SpeedtestBenchmarking;
use App\Models\Result;
Expand Down Expand Up @@ -45,13 +46,14 @@ public function handle(): void

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

if (count($benchmarks) > 0) {
$this->result->update([
'benchmarks' => $benchmarks,
]);
} else {
if (! count($benchmarks)) {
return;
}

$this->result->update([
'benchmarks' => $benchmarks,
'healthy' => EvaluateResultHealth::run($this->result, $benchmarks),
]);
}

private function buildBenchmarks(ThresholdSettings $settings): array
Expand Down
1 change: 1 addition & 0 deletions app/Models/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ protected function casts(): array
return [
'benchmarks' => 'array',
'data' => 'array',
'healthy' => 'boolean',
'service' => ResultService::class,
'status' => ResultStatus::class,
'scheduled' => 'boolean',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?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::table('results', function (Blueprint $table) {
$table->boolean('healthy')
->nullable()
->after('benchmarks');
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
//
}
};