Added error handling of failed speedtests

re issue #143
This commit is contained in:
Henry Whitaker
2020-07-03 11:59:26 +01:00
parent 5ac9478799
commit 04d8f729b9
16 changed files with 318 additions and 37 deletions

View File

@@ -1,6 +1,6 @@
# Speedtest Tracker
[![Docker pulls](https://img.shields.io/docker/pulls/henrywhitaker3/speedtest-tracker?style=flat-square)](https://hub.docker.com/r/henrywhitaker3/speedtest-tracker) [![last_commit](https://img.shields.io/github/last-commit/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/commits) [![issues](https://img.shields.io/github/issues/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/issues) [![commit_freq](https://img.shields.io/github/commit-activity/m/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/commits) ![version](https://img.shields.io/badge/version-v1.7.2-success?style=flat-square) [![license](https://img.shields.io/github/license/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/blob/master/LICENSE)
[![Docker pulls](https://img.shields.io/docker/pulls/henrywhitaker3/speedtest-tracker?style=flat-square)](https://hub.docker.com/r/henrywhitaker3/speedtest-tracker) [![last_commit](https://img.shields.io/github/last-commit/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/commits) [![issues](https://img.shields.io/github/issues/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/issues) [![commit_freq](https://img.shields.io/github/commit-activity/m/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/commits) ![version](https://img.shields.io/badge/version-v1.7.3-success?style=flat-square) [![license](https://img.shields.io/github/license/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/blob/master/LICENSE)
This program runs a speedtest check every hour and graphs the results. The back-end is written in [Laravel](https://laravel.com/) and the front-end uses [React](https://reactjs.org/). It uses the [Ookla's speedtest cli](https://www.speedtest.net/apps/cli) package to get the data and uses [Chart.js](https://www.chartjs.org/) to plot the results.

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class SpeedtestFailedEvent
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}

View File

@@ -27,7 +27,7 @@ class SpeedtestHelper {
$output = json_decode($output, true, 512, JSON_THROW_ON_ERROR);
if(!SpeedtestHelper::checkOutputIsComplete($output)) {
return false;
$test = false;
}
$test = Speedtest::create([
@@ -43,11 +43,27 @@ class SpeedtestHelper {
} catch(JsonException $e) {
Log::error('Failed to parse speedtest JSON');
Log::error($output);
$test = false;
} catch(Exception $e) {
Log::error($e->getMessage());
$test = false;
}
return (isset($test)) ? $test : false;
if(!$test) {
Speedtest::create([
'ping' => 0,
'upload' => 0,
'download' => 0,
'failed' => true,
'scheduled' => $scheduled,
]);
}
if(!isset($test) || $test == false) {
return false;
}
return $test;
}
/**
@@ -83,6 +99,7 @@ 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();

View File

@@ -3,12 +3,14 @@
namespace App\Jobs;
use App\Events\SpeedtestCompleteEvent;
use App\Events\SpeedtestFailedEvent;
use App\Helpers\SpeedtestHelper;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class SpeedtestJob implements ShouldQueue
{
@@ -35,7 +37,13 @@ class SpeedtestJob implements ShouldQueue
{
$output = SpeedtestHelper::output();
$speedtest = SpeedtestHelper::runSpeedtest($output, $this->scheduled);
Log::info($speedtest);
if($speedtest == false) {
Log::info('speedtest == false');
event(new SpeedtestFailedEvent());
} else {
event(new SpeedtestCompleteEvent($speedtest));
}
return $speedtest;
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Listeners;
use App\Helpers\SettingsHelper;
use App\Notifications\SpeedtestFailedSlack;
use App\Notifications\SpeedtestFailedTelegram;
use Exception;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class SpeedtestFailedListener
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
if(env('SLACK_WEBHOOK')) {
try {
Notification::route('slack', env('SLACK_WEBHOOK'))
->notify(new SpeedtestFailedSlack());
} catch(Exception $e) {
Log::notice('Your sleck webhook is invalid');
Log::notice($e);
}
}
if(env('TELEGRAM_BOT_TOKEN') && env('TELEGRAM_CHAT_ID')) {
try {
Notification::route(TelegramChannel::class, env('TELEGRAM_CHAT_ID'))
->notify(new SpeedtestFailedTelegram());
} catch(Exception $e) {
Log::notice('Your telegram settings are invalid');
Log::notice($e);
}
}
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
class SpeedtestFailedSlack extends Notification implements ShouldQueue
{
use Queueable;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return [
'slack',
];
}
/**
* Format slack notification
*
* @param mixed $notifiable
* @return SlackMessage
*/
public function toSlack($notifiable)
{
return (new SlackMessage)
->error()
->attachment(function ($attachment) {
$attachment->title('Failed speedtest')
->content('Something went wrong running your speedtest');
});
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use NotificationChannels\Telegram\TelegramChannel;
use NotificationChannels\Telegram\TelegramMessage;
class SpeedtestFailedTelegram extends Notification implements ShouldQueue
{
use Queueable;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct()
{
//
}
public function via($notifiable)
{
return [
TelegramChannel::class
];
}
/**
* Format tekegram notification
*
* @param mixed $notifiable
* @return TelegramMessage
*/
public function toTelegram($notifiable)
{
$msg = "Error: something went wrong running your speedtest";
return TelegramMessage::create()
->to(env('TELEGRAM_CHAT_ID'))
->content($msg)
->options(['parse_mode' => 'Markdown']);
}
}

View File

@@ -3,8 +3,10 @@
namespace App\Providers;
use App\Events\SpeedtestCompleteEvent;
use App\Events\SpeedtestFailedEvent;
use App\Events\SpeedtestOverviewEvent;
use App\Listeners\SpeedtestCompleteListener;
use App\Listeners\SpeedtestFailedListener;
use App\Listeners\SpeedtestOverviewListener;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
@@ -28,6 +30,9 @@ class EventServiceProvider extends ServiceProvider
SpeedtestOverviewEvent::class => [
SpeedtestOverviewListener::class
],
SpeedtestFailedEvent::class => [
SpeedtestFailedListener::class
],
];
/**

View File

@@ -21,6 +21,7 @@ class Speedtest extends Model
'server_host',
'url',
'scheduled',
'failed',
];
protected $table = 'speedtests';

View File

@@ -1,4 +1,14 @@
{
"1.7.3": [
{
"description": "Updated dependencies",
"link": ""
},
{
"description": "Added notifications and logging of failed tests",
"link": ""
}
],
"1.7.2": [
{
"description": "Updated UI for speedtest info",

View File

@@ -7,7 +7,7 @@ return [
|--------------------------------------------------------------------------
*/
'version' => '1.7.2',
'version' => '1.7.3',
/*
|--------------------------------------------------------------------------

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class UpdateSpeedtestAddFailedColumn extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('speedtests', function($table) {
$table->boolean('failed')->default(false);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('speedtests', function($table) {
$table->dropColumn('failed');
});
}
}

2
public/js/app.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -112,6 +112,7 @@ export default class LatestResults extends Component {
value={parseFloat(data.data.ping).toFixed(1)}
avg={parseFloat(data.average.ping).toFixed(1)}
max={parseFloat(data.max.ping).toFixed(1)}
failed={data.data.failed}
unit="ms"
icon="ping"
/>
@@ -127,6 +128,7 @@ export default class LatestResults extends Component {
value={parseFloat(data.data.download).toFixed(1)}
avg={parseFloat(data.average.download).toFixed(1)}
max={parseFloat(data.max.download).toFixed(1)}
failed={data.data.failed}
unit="Mbit/s"
icon="dl"
/>
@@ -142,6 +144,7 @@ export default class LatestResults extends Component {
value={parseFloat(data.data.upload).toFixed(1)}
avg={parseFloat(data.average.upload).toFixed(1)}
max={parseFloat(data.max.upload).toFixed(1)}
failed={data.data.failed}
unit="Mbit/s"
icon="ul"
/>

View File

@@ -29,6 +29,7 @@ export default class TableRow extends Component {
var e = this.state.data;
var show = this.state.show;
if(e.failed != true) {
return (
<tr>
<td>{e.id}</td>
@@ -59,6 +60,18 @@ export default class TableRow extends Component {
}
</tr>
);
} else {
return (
<tr>
<td>{e.id}</td>
<td>{new Date(e.created_at).toLocaleString()}</td>
<td><span className="ti-close text-danger"></span></td>
<td><span className="ti-close text-danger"></span></td>
<td><span className="ti-close text-danger"></span></td>
<td></td>
</tr>
);
}
}
}

View File

@@ -12,7 +12,8 @@ export default class Widget extends Component {
unit: this.props.unit,
icon: this.props.icon,
avg: this.props.avg,
max: this.props.max
max: this.props.max,
failed: this.props.failed,
}
}
@@ -36,6 +37,7 @@ export default class Widget extends Component {
var icon = this.state.icon;
var max = this.state.max;
var avg = this.state.avg;
var failed = Boolean(Number(this.state.failed));
switch(icon) {
case 'ping':
@@ -60,7 +62,7 @@ export default class Widget extends Component {
</div>
<div className="text-truncate">
<h3 className="d-inline">{value}</h3>
<h3 className="d-inline">{(!failed) ? value : <span className="ti-close text-danger"></span> }</h3>
<p className="d-inline ml-2">{unit} (current)</p>
</div>
<div className="text-muted text-truncate">