diff --git a/app/Actions/QueueSpeedtest.php b/app/Actions/QueueSpeedtest.php new file mode 100644 index 00000000..e52038e8 --- /dev/null +++ b/app/Actions/QueueSpeedtest.php @@ -0,0 +1,35 @@ +speedtestProvider = $speedtestProvider; + } + + /** + * Run the action. + * + * @return mixed + */ + public function run() + { + SettingsHelper::loadIntegrationConfig(); + + SpeedtestJob::dispatch(false, config('integrations'), $this->speedtestProvider); + } +} diff --git a/app/Exceptions/SpeedtestFailureException.php b/app/Exceptions/SpeedtestFailureException.php new file mode 100644 index 00000000..ca668814 --- /dev/null +++ b/app/Exceptions/SpeedtestFailureException.php @@ -0,0 +1,10 @@ + $output['ping']['latency'], - 'download' => SpeedtestHelper::convert($output['download']['bandwidth']), - 'upload' => SpeedtestHelper::convert($output['upload']['bandwidth']), - 'server_id' => $output['server']['id'], - 'server_name' => $output['server']['name'], - 'server_host' => $output['server']['host'] . ':' . $output['server']['port'], - 'url' => $output['result']['url'], - 'scheduled' => $scheduled - ]); - } catch(JsonException $e) { - Log::error('Failed to parse speedtest JSON'); - Log::error($output); - $test = false; - } catch(Exception $e) { - Log::error($e->getMessage()); - $test = false; - } - - if($test == false) { - Speedtest::create([ - 'ping' => 0, - 'upload' => 0, - 'download' => 0, - 'failed' => true, - 'scheduled' => $scheduled, - ]); - - return false; - } - - Cache::flush(); - - return $test; - } - - /** - * Gets the output of executing speedtest binary. - * - * @return boolean|string - */ - public static function output() - { - $server = SettingsHelper::get('server')['value']; - - $binPath = app_path() . DIRECTORY_SEPARATOR . 'Bin' . DIRECTORY_SEPARATOR . 'speedtest'; - $homePrefix = config('speedtest.home') . ' && '; - - if($server != '' && $server != false) { - $server = explode(',', $server); - $server = $server[array_rand($server)]; - if($server == false) { - Log::error('Speedtest server undefined'); - return false; - } - - return shell_exec($homePrefix . $binPath . ' -f json -s ' . $server); - } - - return shell_exec($homePrefix . $binPath . ' -f json'); + $tester = new OoklaTester(); + return $tester->run(); } /** @@ -105,10 +38,10 @@ class SpeedtestHelper { { $t = Carbon::now()->subDay(); $s = Speedtest::select(DB::raw('AVG(ping) as ping, AVG(download) as download, AVG(upload) as upload')) - ->where('created_at', '>=', $t) - ->where('failed', false) - ->first() - ->toArray(); + ->where('created_at', '>=', $t) + ->where('failed', false) + ->first() + ->toArray(); return $s; } @@ -119,8 +52,9 @@ class SpeedtestHelper { * @param int|float $bytes * @return int|float */ - public static function convert($bytes) { - return ( $bytes * 8 ) / 1000000; + public static function convert($bytes) + { + return ($bytes * 8) / 1000000; } /** @@ -132,7 +66,7 @@ class SpeedtestHelper { { $data = Speedtest::latest()->get(); - if($data->isEmpty()) { + if ($data->isEmpty()) { return false; } @@ -152,7 +86,7 @@ class SpeedtestHelper { $val = (float)$input[0]; $unit = explode('/', $input[1])[0]; - switch($unit) { + switch ($unit) { case 'Mbyte': $val = $val * 8; break; @@ -173,48 +107,6 @@ class SpeedtestHelper { ]; } - /** - * Checks that the speedtest JSON output is complete/valid - * - * @param array $output - * @return boolean - */ - public static function checkOutputIsComplete($output) - { - /** - * Array of indexes that must exist in $output - */ - $checks = [ - 'type' => 'result', - 'download' => [ 'bandwidth' => '*' ], - 'upload' => [ 'bandwidth' => '*' ], - 'ping' => [ 'latency' => '*' ], - 'server' => [ - 'id' => '*', - 'name' => '*', - 'host' => '*', - 'port' => '*' - ], - 'result' => [ - 'url' => '*' - ], - ]; - /** - * Array of indexes that must not exist - */ - $checkMissing = [ - 'type' => 'error' - ]; - - foreach($checks as $key => $value) { - if(!isset($output[$key])) { - return false; - } - } - - return true; - } - /** * Get a percentage rate of failure by days * @@ -228,7 +120,7 @@ class SpeedtestHelper { $range = [ Carbon::today() ]; - for($i = 0; $i < ($days - 1); $i++) { + for ($i = 0; $i < ($days - 1); $i++) { $prev = end($range); $new = $prev->copy()->subDays(1); array_push($range, $new); @@ -236,7 +128,7 @@ class SpeedtestHelper { $rate = []; - foreach($range as $day) { + foreach ($range as $day) { $success = Speedtest::select(DB::raw('COUNT(id) as rate'))->whereDate('created_at', $day)->where('failed', false)->get()[0]['rate']; $fail = Speedtest::select(DB::raw('COUNT(id) as rate'))->whereDate('created_at', $day)->where('failed', true)->get()[0]['rate']; @@ -260,14 +152,14 @@ class SpeedtestHelper { */ public static function dbBackup() { - if(env('DB_CONNECTION') === 'sqlite') { - if(env('DB_DATABASE') !== null) { + if (env('DB_CONNECTION') === 'sqlite') { + if (env('DB_DATABASE') !== null) { $current = env('DB_DATABASE'); try { - if(File::copy($current, $current . '.bak')) { + if (File::copy($current, $current . '.bak')) { return true; } - }catch(Exception $e) { + } catch (Exception $e) { return false; } } @@ -289,8 +181,8 @@ class SpeedtestHelper { SpeedtestHelper::dbBackup(); - if(sizeof(Speedtest::whereNotNull('id')->get()) > 0) { - if(Speedtest::whereNotNull('id')->delete()) { + if (sizeof(Speedtest::whereNotNull('id')->get()) > 0) { + if (Speedtest::whereNotNull('id')->delete()) { return [ 'success' => true, ]; @@ -311,31 +203,31 @@ class SpeedtestHelper { */ public static function testIsLowerThanThreshold(String $type, Speedtest $test) { - if($type == 'percentage') { + if ($type == 'percentage') { $avg = Speedtest::select(DB::raw('AVG(ping) as ping, AVG(download) as download, AVG(upload) as upload')) - ->where('failed', false) - ->get() - ->toArray()[0]; + ->where('failed', false) + ->get() + ->toArray()[0]; $threshold = SettingsHelper::get('threshold_alert_percentage')->value; - if($threshold == '') { + if ($threshold == '') { return []; } $errors = []; - foreach($avg as $key => $value) { - if($key == 'ping') { - $threshold = (float)$value * (1 + ( $threshold / 100 )); + foreach ($avg as $key => $value) { + if ($key == 'ping') { + $threshold = (float)$value * (1 + ($threshold / 100)); - if($test->$key > $threshold) { + if ($test->$key > $threshold) { array_push($errors, $key); } - } else { - $threshold = (float)$value * (1 - ( $threshold / 100 )); + } else { + $threshold = (float)$value * (1 - ($threshold / 100)); - if($test->$key < $threshold) { + if ($test->$key < $threshold) { array_push($errors, $key); } } @@ -344,7 +236,7 @@ class SpeedtestHelper { return $errors; } - if($type == 'absolute') { + if ($type == 'absolute') { $thresholds = [ 'download' => SettingsHelper::get('threshold_alert_absolute_download')->value, 'upload' => SettingsHelper::get('threshold_alert_absolute_upload')->value, @@ -353,17 +245,17 @@ class SpeedtestHelper { $errors = []; - foreach($thresholds as $key => $value) { - if($value == '') { + foreach ($thresholds as $key => $value) { + if ($value == '') { continue; } - if($key == 'ping') { - if($test->$key > $value) { + if ($key == 'ping') { + if ($test->$key > $value) { array_push($errors, $key); } } else { - if($test->$key < $value) { + if ($test->$key < $value) { array_push($errors, $key); } } diff --git a/app/Http/Controllers/SpeedtestController.php b/app/Http/Controllers/SpeedtestController.php index ddb4b719..900616c7 100644 --- a/app/Http/Controllers/SpeedtestController.php +++ b/app/Http/Controllers/SpeedtestController.php @@ -2,6 +2,9 @@ namespace App\Http\Controllers; +use App\Actions\GetLatestSpeedtestData; +use App\Actions\GetSpeedtestTimeData; +use App\Actions\QueueSpeedtest; use App\Helpers\SettingsHelper; use App\Helpers\SpeedtestHelper; use App\Jobs\SpeedtestJob; @@ -61,21 +64,7 @@ class SpeedtestController extends Controller ], 422); } - $ttl = Carbon::now()->addDays(1); - $data = Cache::remember('speedtest-days-' . $days, $ttl, function () use ($days) { - $showFailed = (bool)SettingsHelper::get('show_failed_tests_on_graph')->value; - - if ($showFailed === true) { - return Speedtest::where('created_at', '>=', Carbon::now()->subDays($days)) - ->orderBy('created_at', 'asc') - ->get(); - } - - return Speedtest::where('created_at', '>=', Carbon::now()->subDays($days)) - ->where('failed', false) - ->orderBy('created_at', 'asc') - ->get(); - }); + $data = run(GetSpeedtestTimeData::class, $days); return response()->json([ 'method' => 'get speedtests in last x days', @@ -121,39 +110,10 @@ class SpeedtestController extends Controller */ public function latest() { - $data = SpeedtestHelper::latest(); + $data = run(GetLatestSpeedtestData::class); - $response = [ - 'method' => 'get latest speedtest', - 'data' => $data, - ]; - - if (SettingsHelper::get('show_average')) { - $avg = Speedtest::select(DB::raw('AVG(ping) as ping, AVG(download) as download, AVG(upload) as upload')) - ->where('failed', false) - ->first() - ->toArray(); - $response['average'] = $avg; - } - - if (SettingsHelper::get('show_max')) { - $max = Speedtest::select(DB::raw('MAX(ping) as ping, MAX(download) as download, MAX(upload) as upload')) - ->where('failed', false) - ->first() - ->toArray(); - $response['maximum'] = $max; - } - - if (SettingsHelper::get('show_min')) { - $min = Speedtest::select(DB::raw('MIN(ping) as ping, MIN(download) as download, MIN(upload) as upload')) - ->where('failed', false) - ->first() - ->toArray(); - $response['minimum'] = $min; - } - - if ($data) { - return response()->json($response, 200); + if ($data['data']) { + return response()->json($data, 200); } else { return response()->json([ 'method' => 'get latest speedtest', @@ -170,8 +130,8 @@ class SpeedtestController extends Controller public function run() { try { - SettingsHelper::loadIntegrationConfig(); - $data = SpeedtestJob::dispatch(false, config('integrations')); + run(QueueSpeedtest::class); + return response()->json([ 'method' => 'run speedtest', 'data' => 'a new speedtest has been added to the queue' diff --git a/app/Interfaces/SpeedtestProvider.php b/app/Interfaces/SpeedtestProvider.php new file mode 100644 index 00000000..f0124d7f --- /dev/null +++ b/app/Interfaces/SpeedtestProvider.php @@ -0,0 +1,11 @@ +scheduled = $scheduled; $this->config = $config; + $this->speedtestProvider = $speedtestProvider; } /** @@ -55,8 +60,8 @@ class SpeedtestJob implements ShouldQueue if ($this->config['healthchecks_enabled'] === true) { $this->healthcheck('start'); } - $output = SpeedtestHelper::output(); - $speedtest = SpeedtestHelper::runSpeedtest($output, $this->scheduled); + $output = $this->speedtestProvider->output(); + $speedtest = $this->speedtestProvider->run($output, $this->scheduled); if ($speedtest == false) { if ($this->config['healthchecks_enabled'] === true) { $this->healthcheck('fail'); diff --git a/app/Providers/SpeedtestServiceProvider.php b/app/Providers/SpeedtestServiceProvider.php new file mode 100644 index 00000000..83615bb3 --- /dev/null +++ b/app/Providers/SpeedtestServiceProvider.php @@ -0,0 +1,34 @@ +app->singleton(SpeedtestProvider::class, function () { + return new OoklaTester(); + }); + break; + } + } + } + } +} diff --git a/app/Utils/OoklaTester.php b/app/Utils/OoklaTester.php new file mode 100644 index 00000000..41d7ee73 --- /dev/null +++ b/app/Utils/OoklaTester.php @@ -0,0 +1,128 @@ +output(); + } + + try { + $output = json_decode($output, true, 512, JSON_THROW_ON_ERROR); + + if (!$this->isOutputIsComplete($output)) { + $test = false; + } + + $test = Speedtest::create([ + 'ping' => $output['ping']['latency'], + 'download' => SpeedtestHelper::convert($output['download']['bandwidth']), + 'upload' => SpeedtestHelper::convert($output['upload']['bandwidth']), + 'server_id' => $output['server']['id'], + 'server_name' => $output['server']['name'], + 'server_host' => $output['server']['host'] . ':' . $output['server']['port'], + 'url' => $output['result']['url'], + 'scheduled' => $scheduled + ]); + } catch (JsonException $e) { + Log::error('Failed to parse speedtest JSON'); + Log::error($output); + $test = false; + } catch (Exception $e) { + Log::error($e->getMessage()); + $test = false; + } + + if ($test == false) { + Speedtest::create([ + 'ping' => 0, + 'upload' => 0, + 'download' => 0, + 'failed' => true, + 'scheduled' => $scheduled, + ]); + + throw new SpeedtestFailureException($output); + } + + Cache::flush(); + + return $test; + } + + public function output() + { + $server = SettingsHelper::get('server')['value']; + + $binPath = app_path() . DIRECTORY_SEPARATOR . 'Bin' . DIRECTORY_SEPARATOR . 'speedtest'; + $homePrefix = config('speedtest.home') . ' && '; + + if ($server != '' && $server != false) { + $server = explode(',', $server); + $server = $server[array_rand($server)]; + if ($server == false) { + Log::error('Speedtest server undefined'); + return false; + } + + return shell_exec($homePrefix . $binPath . ' -f json -s ' . $server); + } + + return shell_exec($homePrefix . $binPath . ' -f json'); + } + + /** + * Checks that the speedtest JSON output is complete/valid + * + * @param array $output + * @return boolean + */ + private static function isOutputIsComplete($output) + { + /** + * Array of indexes that must exist in $output + */ + $checks = [ + 'type' => 'result', + 'download' => ['bandwidth' => '*'], + 'upload' => ['bandwidth' => '*'], + 'ping' => ['latency' => '*'], + 'server' => [ + 'id' => '*', + 'name' => '*', + 'host' => '*', + 'port' => '*' + ], + 'result' => [ + 'url' => '*' + ], + ]; + /** + * Array of indexes that must not exist + */ + $checkMissing = [ + 'type' => 'error' + ]; + + foreach ($checks as $key => $value) { + if (!isset($output[$key])) { + return false; + } + } + + return true; + } +} diff --git a/config/app.php b/config/app.php index 8c3685ce..b3abb874 100644 --- a/config/app.php +++ b/config/app.php @@ -182,6 +182,7 @@ return [ * Custom providers... */ App\Providers\IntegrationsServiceProvider::class, + App\Providers\SpeedtestServiceProvider::class, ], diff --git a/database/migrations/2021_03_07_101259_add_speedtest_provider_setting.php b/database/migrations/2021_03_07_101259_add_speedtest_provider_setting.php new file mode 100644 index 00000000..4510dc09 --- /dev/null +++ b/database/migrations/2021_03_07_101259_add_speedtest_provider_setting.php @@ -0,0 +1,38 @@ + 'speedtest_provider', + 'value' => 'ookla', + 'description' => 'The provider/package used to run speedtests.' + ]); + } + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Setting::whereIn('name', [ + 'speedtest_provider', + ])->delete(); + } +}