Added failure graph and graph settings panel

This commit is contained in:
Henry Whitaker
2020-07-06 19:34:10 +01:00
parent 6efc1d0560
commit f543e1345c
16 changed files with 143099 additions and 24 deletions

View File

@@ -1,6 +1,6 @@
# Speedtest Tracker # 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.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) [![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.4-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. 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

@@ -3,6 +3,7 @@
namespace App\Helpers; namespace App\Helpers;
use App\Setting; use App\Setting;
use Carbon\Carbon;
class SettingsHelper { class SettingsHelper {
@@ -38,6 +39,9 @@ class SettingsHelper {
$setting = SettingsHelper::get($name); $setting = SettingsHelper::get($name);
if($setting !== false) { if($setting !== false) {
if($value == false) {
$value = "0";
}
$setting->value = $value; $setting->value = $value;
$setting->save(); $setting->save();
} else { } else {

View File

@@ -5,6 +5,7 @@ namespace App\Helpers;
use App\Speedtest; use App\Speedtest;
use Carbon\Carbon; use Carbon\Carbon;
use Exception; use Exception;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use JsonException; use JsonException;
@@ -63,6 +64,8 @@ class SpeedtestHelper {
return false; return false;
} }
Cache::flush();
return $test; return $test;
} }
@@ -207,4 +210,47 @@ class SpeedtestHelper {
return true; return true;
} }
/**
* Get a percentage rate of failure by days
*
* @param integer $days number of days to get rate for
* @return integer percentage fail rate
*/
public static function failureRate(int $days)
{
$ttl = Carbon::now()->addDays(1);
$rate = Cache::remember('failure-rate-' . $days, $ttl, function () use ($days) {
$range = [
Carbon::today()
];
for($i = 0; $i < $days; $i++) {
$prev = end($range);
$new = $prev->copy()->subDays(1);
array_push($range, $new);
}
$rate = [];
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'];
if(( $success + $fail ) == 0) {
$percentage = 0;
} else {
$percentage = round(( $fail / ( $success + $fail ) * 100 ), 1);
}
array_push($rate, [
'date' => $day->toDateString(),
'rate' => $percentage
]);
}
return array_reverse($rate);
});
return $rate;
}
} }

View File

@@ -120,7 +120,13 @@ class SettingsController extends Controller
$config = [ $config = [
'base' => SettingsHelper::getBase() 'base' => SettingsHelper::getBase(),
'download_upload_graph_enabled' => SettingsHelper::get('download_upload_graph_enabled'),
'download_upload_graph_width' => SettingsHelper::get('download_upload_graph_width'),
'ping_graph_enabled' => SettingsHelper::get('ping_graph_enabled'),
'ping_graph_width' => SettingsHelper::get('ping_graph_width'),
'failure_graph_enabled' => SettingsHelper::get('failure_graph_enabled'),
'failure_graph_width' => SettingsHelper::get('failure_graph_width'),
]; ];
return $config; return $config;

View File

@@ -8,6 +8,7 @@ use App\Speedtest;
use Carbon\Carbon; use Carbon\Carbon;
use Exception; use Exception;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
@@ -51,10 +52,43 @@ class SpeedtestController extends Controller
], 422); ], 422);
} }
$data = Speedtest::where('created_at', '>=', Carbon::now()->subDays($days)) $ttl = Carbon::now()->addDays(1);
->where('failed', false) $data = Cache::remember('speedtest-days-' . $days, $ttl, function () use ($days) {
->orderBy('created_at', 'asc') return Speedtest::where('created_at', '>=', Carbon::now()->subDays($days))
->get(); ->where('failed', false)
->orderBy('created_at', 'asc')
->get();
});
return response()->json([
'method' => 'get speedtests in last x days',
'days' => $days,
'data' => $data
], 200);
}
/**
* Returns speedtest failure rate going back 'x' days
*
* @param int $days
* @return void
*/
public function fail($days)
{
$rule = [
'days' => [ 'required', 'integer' ],
];
$validator = Validator::make([ 'days' => $days ], $rule);
if($validator->fails()) {
return response()->json([
'method' => 'get speedtests in last x days',
'error' => $validator->errors(),
], 422);
}
$data = SpeedtestHelper::failureRate($days);
return response()->json([ return response()->json([
'method' => 'get speedtests in last x days', 'method' => 'get speedtests in last x days',

View File

@@ -39,7 +39,7 @@ class Kernel extends HttpKernel
], ],
'api' => [ 'api' => [
'throttle:100,1', 'throttle:200,1',
\Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Routing\Middleware\SubstituteBindings::class,
], ],
]; ];

View File

@@ -1,4 +1,18 @@
{ {
"1.7.4": [
{
"description": "Stopped failed tests appearing in graphs",
"link": ""
},
{
"description": "Added failure rate graph",
"link": ""
},
{
"description": "Updated dependencies",
"link": ""
}
],
"1.7.3": [ "1.7.3": [
{ {
"description": "Updated dependencies", "description": "Updated dependencies",

View File

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

View File

@@ -0,0 +1,70 @@
<?php
use App\Setting;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddGraphSettings extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Setting::create([
'name' => 'download_upload_graph_enabled',
'value' => true,
'description' => 'Enable the download/upload graph'
]);
Setting::create([
'name' => 'download_upload_graph_width',
'value' => 6,
'description' => 'Set the width of the download/upload graph'
]);
Setting::create([
'name' => 'ping_graph_enabled',
'value' => true,
'description' => 'Enable the ping graph'
]);
Setting::create([
'name' => 'ping_graph_width',
'value' => 6,
'description' => 'Set the width of the ping graph'
]);
Setting::create([
'name' => 'failure_graph_enabled',
'value' => true,
'description' => 'Enable the failure rate graph'
]);
Setting::create([
'name' => 'failure_graph_width',
'value' => 6,
'description' => 'Set the width of the failure rate graph'
]);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Setting::whereIn('name', [
'download_upload_graph_enabled',
'download_upload_graph_width',
'ping_graph_enabled',
'ping_graph_width',
'failure_graph_enabled',
'failure_graph_width'
])->delete();
}
}

10931
public/css/app.css vendored

File diff suppressed because one or more lines are too long

4
public/css/main.css vendored
View File

@@ -55,3 +55,7 @@
.setting-card { .setting-card {
height: 270px; height: 270px;
} }
.home-graph {
height: 480px;
}

131746
public/js/app.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -16,8 +16,16 @@ export default class HistoryGraph extends Component {
duOptions: {}, duOptions: {},
pingData: {}, pingData: {},
pingOptions: {}, pingOptions: {},
failData: {},
failOptions: {},
loading: true, loading: true,
interval: null, interval: null,
graph_ul_dl_enabled: true,
graph_ul_dl_width: 6,
graph_failure_enabled: true,
graph_failure_width: 6,
graph_ping_enabled: true,
graph_ping_width: 6,
} }
} }
@@ -29,7 +37,7 @@ export default class HistoryGraph extends Component {
}); });
} }
getData = (days = this.state.days) => { getDLULPing = (days) => {
var url = 'api/speedtest/time/' + days; var url = 'api/speedtest/time/' + days;
Axios.get(url) Axios.get(url)
@@ -52,6 +60,7 @@ export default class HistoryGraph extends Component {
], ],
}; };
var duOptions = { var duOptions = {
maintainAspectRatio: false,
tooltips: { tooltips: {
callbacks: { callbacks: {
label: (item) => `${item.yLabel} Mbit/s`, label: (item) => `${item.yLabel} Mbit/s`,
@@ -90,6 +99,7 @@ export default class HistoryGraph extends Component {
], ],
}; };
var pingOptions = { var pingOptions = {
maintainAspectRatio: false,
tooltips: { tooltips: {
callbacks: { callbacks: {
label: (item) => `${item.yLabel} ms`, label: (item) => `${item.yLabel} ms`,
@@ -149,6 +159,97 @@ export default class HistoryGraph extends Component {
}) })
} }
getFailure = (days) => {
var url = 'api/speedtest/fail/' + days;
Axios.get(url)
.then((resp) => {
var failData = {
labels: [],
datasets:[
{
data: [],
label: 'Failure',
borderColor: "#E74C3C",
fill: false,
},
],
};
var failOptions = {
maintainAspectRatio: false,
tooltips: {
callbacks: {
label: (item) => `${item.yLabel} %`,
},
},
title: {
display: false,
text: 'Ping results for the last ' + days + ' days',
},
scales: {
xAxes: [{
display: false,
scaleLabel: {
display: true,
labelString: 'DateTime'
}
}],
},
elements: {
point:{
radius: 0,
hitRadius: 8
}
}
}
resp.data.data.forEach(e => {
var date = new Date(e.date);
var fail = {
t: date,
y: e.rate
};
failData.datasets[0].data.push(fail);
failData.labels.push(date.getFullYear() + '/' + ('0' + (date.getMonth() + 1)).slice(-2) + '/' + ('0' + date.getDay()).slice(-2));
});
this.setState({
failData: failData,
failOptions: failOptions
});
})
.catch((err) => {
console.log(err);
})
}
getData = (days = this.state.days) => {
Axios.get('api/settings/config')
.then((resp) => {
var data = resp.data;
console.log(data)
this.setState({
graph_ul_dl_enabled: Boolean(Number(data.download_upload_graph_enabled.value)),
graph_ul_dl_width: data.download_upload_graph_width.value,
graph_ping_enabled: Boolean(Number(data.ping_graph_enabled.value)),
graph_ping_width: data.ping_graph_width.value,
graph_failure_enabled: Boolean(Number(data.failure_graph_enabled.value)),
graph_failure_width: data.failure_graph_width.value,
});
if(this.state.graph_ul_dl_enabled || this.state.graph_ping_enabled) {
this.getDLULPing(days);
}
if(this.state.graph_failure_enabled) {
this.getFailure(days);
}
})
.catch((err) => {
console.log('Couldn\'t get the site config');
console.log(err);
})
}
updateDays = (e) => { updateDays = (e) => {
var days = e.target.value; var days = e.target.value;
if(days) { if(days) {
@@ -169,8 +270,44 @@ export default class HistoryGraph extends Component {
var duOptions = this.state.duOptions; var duOptions = this.state.duOptions;
var pingData = this.state.pingData; var pingData = this.state.pingData;
var pingOptions = this.state.pingOptions; var pingOptions = this.state.pingOptions;
var failData = this.state.failData;
var failOptions = this.state.failOptions;
var days = this.state.days; var days = this.state.days;
console.log(failData);
console.log(failOptions);
console.log(pingData);
console.log(pingOptions);
var graph_ul_dl_enabled = this.state.graph_ul_dl_enabled;
var graph_ul_dl_width = this.state.graph_ul_dl_width;
var graph_ping_enabled = this.state.graph_ping_enabled;
var graph_ping_width = this.state.graph_ping_width;
var graph_failure_enabled = this.state.graph_failure_enabled;
var graph_failure_width = this.state.graph_failure_width;
var dlClasses = 'my-2 home-graph ';
var pingClasses = 'my-2 home-graph ';
var failureClasses = 'my-2 home-graph ';
if(graph_ul_dl_enabled == true) {
//
} else {
dlClasses += 'd-none ';
}
if(graph_ping_enabled == true) {
//
} else {
pingClasses += 'd-none ';
}
if(graph_failure_enabled == true) {
//
} else {
failureClasses += 'd-none ';
}
if(loading) { if(loading) {
return ( return (
<div> <div>
@@ -180,31 +317,43 @@ export default class HistoryGraph extends Component {
} else { } else {
return ( return (
<Container className="mb-4 mt-1" fluid> <Container className="mb-4 mt-1" fluid>
<Row> <Row>
<Col <Col
lg={{ span: 6 }} lg={{ span: graph_ul_dl_width }}
md={{ span: 12 }} md={{ span: graph_ul_dl_width }}
sm={{ span: 12 }} sm={{ span: 12 }}
xs={{ span: 12 }} xs={{ span: 12 }}
className="my-2" className={dlClasses}
> >
<Card className="shadow-sm"> <Card className="shadow-sm">
<Card.Body> <Card.Body>
<Line data={duData} options={duOptions} /> <Line data={duData} options={duOptions} height={440} />
</Card.Body> </Card.Body>
</Card> </Card>
</Col> </Col>
<Col <Col
lg={{ span: 6 }} lg={{ span: graph_ping_width }}
md={{ span: 12 }} md={{ span: graph_ping_width }}
sm={{ span: 12 }} sm={{ span: 12 }}
xs={{ span: 12 }} xs={{ span: 12 }}
className="my-2" className={pingClasses}
> >
<Card className="shadow-sm"> <Card className="shadow-sm">
<Card.Body> <Card.Body>
<Line data={pingData} options={pingOptions} /> <Line data={pingData} options={pingOptions} height={440} />
</Card.Body>
</Card>
</Col>
<Col
lg={{ span: graph_failure_width }}
md={{ span: graph_failure_width }}
sm={{ span: 12 }}
xs={{ span: 12 }}
className={failureClasses}
>
<Card className="shadow-sm">
<Card.Body>
<Line data={failData} options={pingOptions} height={440} />
</Card.Body> </Card.Body>
</Card> </Card>
</Col> </Col>
@@ -217,7 +366,6 @@ export default class HistoryGraph extends Component {
<Form.Control id="duDaysInput" className="d-inline-block mx-2" defaultValue={days} onInput={this.updateDays}></Form.Control> <Form.Control id="duDaysInput" className="d-inline-block mx-2" defaultValue={days} onInput={this.updateDays}></Form.Control>
<h4 className="d-inline mb-0">days</h4> <h4 className="d-inline mb-0">days</h4>
</div> </div>
{/* <p className="text-muted">This data refreshes every 10 seconds</p> */}
</div> </div>
</Col> </Col>
</Row> </Row>

View File

@@ -116,7 +116,7 @@ export default class SettingWithModal extends Component {
<Row key={e.obj.id} className="d-flex align-items-center"> <Row key={e.obj.id} className="d-flex align-items-center">
<Col md={{ span: 6 }} sm={{ span: 12 }}> <Col md={{ span: 6 }} sm={{ span: 12 }}>
<Form.Group controlId={e.obj.name}> <Form.Group controlId={e.obj.name}>
<Form.Check type="checkbox" label={name} defaultChecked={e.obj.value} onInput={this.updateValue} /> <Form.Check type="checkbox" label={name} defaultChecked={Boolean(Number(e.obj.value))} onInput={this.updateValue} />
</Form.Group> </Form.Group>
</Col> </Col>
<Col md={{ span: 6 }} sm={{ span: 12 }}> <Col md={{ span: 6 }} sm={{ span: 12 }}>
@@ -138,6 +138,26 @@ export default class SettingWithModal extends Component {
</Col> </Col>
</Row> </Row>
); );
} else if(e.type == 'select') {
return (
<Row key={e.obj.id}>
<Col md={{ span: 6 }} sm={{ span: 12 }}>
<Form.Group controlId={e.obj.name}>
<Form.Label>{name}</Form.Label>
<Form.Control as="select" defaultValue={e.obj.value} onInput={this.updateValue}>
{e.options.map((e,i) => {
return (
<option key={i} value={e.value}>{e.name}</option>
)
})}
</Form.Control>
</Form.Group>
</Col>
<Col md={{ span: 6 }} sm={{ span: 12 }}>
<p>{e.obj.description}</p>
</Col>
</Row>
)
} }
})} })}
<Button variant="primary" type="submit" onClick={this.update} >Save</Button> <Button variant="primary" type="submit" onClick={this.update} >Save</Button>

View File

@@ -60,6 +60,64 @@ export default class Settings extends Component {
<Col lg={{ span: 4 }} md={{ span: 6 }} sm={{ span: 12 }}> <Col lg={{ span: 4 }} md={{ span: 6 }} sm={{ span: 12 }}>
<Setting name={e.server.name} value={e.server.value} description={e.server.description} /> <Setting name={e.server.name} value={e.server.value} description={e.server.description} />
</Col> </Col>
<Col lg={{ span: 4 }} md={{ span: 6 }} sm={{ span: 12 }}>
<SettingWithModal title="Graph settings" description="Control settings for the graphs." settings={[
{
obj: e.download_upload_graph_enabled,
type: 'checkbox'
},
{
obj: e.download_upload_graph_width,
type: 'select',
options: [
{
name: 'Full-width',
'value': 12
},
{
name: 'Half-width',
'value': 6
}
],
},
{
obj: e.ping_graph_enabled,
type: 'checkbox'
},
{
obj: e.ping_graph_width,
type: 'select',
options: [
{
name: 'Full-width',
'value': 12
},
{
name: 'Half-width',
'value': 6
}
],
},
{
obj: e.failure_graph_enabled,
type: 'checkbox'
},
{
obj: e.failure_graph_width,
type: 'select',
options: [
{
name: 'Full-width',
'value': 12
},
{
name: 'Half-width',
'value': 6
}
],
}
]} />
</Col>
<Col lg={{ span: 4 }} md={{ span: 6 }} sm={{ span: 12 }}> <Col lg={{ span: 4 }} md={{ span: 6 }} sm={{ span: 12 }}>
<SettingWithModal title="Notification settings" description="Control which types of notifications the server sends." settings={[ <SettingWithModal title="Notification settings" description="Control which types of notifications the server sends." settings={[
{ {

View File

@@ -25,6 +25,8 @@ Route::group([
->name('speedtest.latest'); ->name('speedtest.latest');
Route::get('time/{time}', 'SpeedtestController@time') Route::get('time/{time}', 'SpeedtestController@time')
->name('speedtest.time'); ->name('speedtest.time');
Route::get('fail/{time}', 'SpeedtestController@fail')
->name('speedtest.fail');
Route::get('run', 'SpeedtestController@run') Route::get('run', 'SpeedtestController@run')
->name('speedtest.run'); ->name('speedtest.run');
}); });