diff --git a/app/Checkers/Uptime.php b/app/Checkers/Uptime.php new file mode 100644 index 0000000..f1f8958 --- /dev/null +++ b/app/Checkers/Uptime.php @@ -0,0 +1,68 @@ +website = $website; + } + + public function run() + { + $this->fetch(); +// $this->compare(); + } + + private function fetch() + { + $client = new Client(); + + $response_time = 3001; + + $response = $client->request('GET', $this->website->url, [ + 'on_stats' => function ($stats) use (&$response_time) { + $response_time = $stats->getTransferTime(); + }, + 'verify' => false, + 'allow_redirects' => true, + 'headers' => [ + 'User-Agent' => 'Mozilla/5.0+(compatible; UptimeRobot/2.0; http://www.uptimerobot.com/; Odin)' + ], + ]); + + $scan = new UptimeScan([ + 'response_status' => sprintf('%s (%d)', $response->getReasonPhrase(), $response->getStatusCode()), + 'response_time' => $response_time, + 'was_online' => Str::contains($response->getBody(), $this->website->uptime_keyword) + ]); + + $this->website->uptimes()->save($scan); + } + + private function compare() + { + $scans = $this->website->last_robot_scans; + + if ($scans->isEmpty() || $scans->count() === 1) { + return; + } + + $diff = (new Differ)->diff($scans->last()->txt, $scans->first()->txt); + + $scans->first()->diff = $diff; + $scans->first()->save(); + } +} diff --git a/app/Console/Commands/UptimeCheckCommand.php b/app/Console/Commands/UptimeCheckCommand.php new file mode 100644 index 0000000..2d280b7 --- /dev/null +++ b/app/Console/Commands/UptimeCheckCommand.php @@ -0,0 +1,48 @@ +argument('website'); + + UptimeCheck::dispatchNow( + Website::findOrFail($websiteId) + ); + } +} diff --git a/app/HasRobots.php b/app/HasRobots.php new file mode 100644 index 0000000..3cefb58 --- /dev/null +++ b/app/HasRobots.php @@ -0,0 +1,25 @@ +hasMany(RobotScan::class); + } + + public function getLastRobotScansAttribute() + { + return $this->robots()->orderBy('created_at', 'desc')->take(2)->get(); + } + + public function getRobotsUrlAttribute() + { + return $this->url . '/robots.txt'; + } + +} diff --git a/app/HasUptime.php b/app/HasUptime.php new file mode 100644 index 0000000..48d8446 --- /dev/null +++ b/app/HasUptime.php @@ -0,0 +1,141 @@ +hasMany(UptimeScan::class)->orderBy('created_at', 'desc'); + } + + public function getRecentEventsAttribute() + { + return $this->uptimes->sortByDesc('created_at')->take(10); + } + + public function getLastIncidentAttribute() + { + $event = $this->uptimes + ->where('was_online', 0) + ->sortByDesc('created_at') + ->first(); + + if (!$event) { + return 'No downtime recorded'; + } + + $downtime = 'xxx'; + $when = $event->created_at->format('D jS M "y @ h:ia'); + + return sprintf('Was down for %s on %s', $downtime, $when); + + return 'Will update later'; + } + + public function getCurrentStateAttribute() + { + $event = $this->uptimes->sortByDesc('created_at')->first(); + + if (!$event) { + return true; + } + + return $event->was_online; + } + + public function getUptimeAttribute() + { + $latest = $this->uptimes->sortByDesc('created_at')->first(); + $online = $this->uptimes->firstWhere('was_online', 1); + $offline = $this->uptimes->firstWhere('was_online', 0); + + if (!$latest) { + return 'Still collecting data...'; + } + + if ($latest->offline) { + if ($offline && $online) { + return now()->diffAsCarbonInterval($online->created_at)->forHumans(['join' => true]); + } + + return 'Site never previously recorded as online.'; + } + + if (!$offline) { + return now()->diffAsCarbonInterval($online->created_at)->forHumans(['join' => true]); + } + + return $offline->created_at->diffAsCarbonInterval(now())->forHumans(['join' => true]); + } + + public function getResponseTimesAttribute() + { + return $this->uptimes + ->sortByDesc('created_at') + ->take(25) + ->transform(function (UptimeScan $scan) { + return [ + 'date' => $scan->created_at, + 'value' => $scan->response_time, + ]; + }) + ->values(); + } + + public function getResponseTimeAttribute() + { + $time = $this->response_times->first(); + + if (!$time) { + return '-'; + } + + return $time['value']; + } + + public function getUptimeSummaryAttribute() + { + /* @var Collection $events */ + $events = $this->uptimes; + + $upCount = $events->where('was_online', 1); + $totalPercentage = ($upCount->count() * 100) / $events->count(); + + $today = now(); + $lastWeek = now()->subDays(7); + $lastMonth = now()->subDays(30); + + $todayEvents = $events->filter(function ($item) use ($today) { + return $item->created_at->format('d/m/y') === $today->format('d/m/y'); + }); + + $todayUpEvents = $todayEvents->where('was_online', 1); + $todayPercentage = ($todayUpEvents->count() * 100) / $todayEvents->count(); + + $weeklyEvents = $events->filter(function ($item) use ($lastWeek, $today) { + return $item->created_at->isBetween($lastWeek, $today); + }); + + $weeklyUpEvents = $weeklyEvents->where('was_online', 1); + $weeklyPercentage = ($weeklyUpEvents->count() * 100) / $weeklyEvents->count(); + + $monthlyEvents = $events->filter(function ($item) use ($lastMonth, $today) { + return $item->created_at->isBetween($lastMonth, $today); + }); + + $monthlyUpEvents = $monthlyEvents->where('was_online', 1); + $monthlyPercentage = ($monthlyUpEvents->count() * 100) / $monthlyEvents->count(); + + return [ + 'total' => floor($totalPercentage), + 'day' => floor($todayPercentage), + 'week' => floor($weeklyPercentage), + 'month' => floor($monthlyPercentage), + ]; + } +} diff --git a/app/Http/Controllers/UptimeReportController.php b/app/Http/Controllers/UptimeReportController.php new file mode 100644 index 0000000..b4bda37 --- /dev/null +++ b/app/Http/Controllers/UptimeReportController.php @@ -0,0 +1,43 @@ +load(['uptimes']); + + $response = [ + 'uptime' => $website->uptime_summary, + 'response_time' => $website->response_time, + 'response_times' => $website->response_times, + 'online' => $website->current_state, + 'online_time' => $website->uptime, + 'last_incident' => $website->last_incident, + 'events' => $website->recent_events->transform(function (UptimeScan $scan) { + return [ + 'id' => $scan->getKey(), + 'date' => $scan->created_at, + 'type' => $scan->was_online ? 'up' : 'down', + 'reason' => $scan->response_status, + 'duration' => 10, + ]; + })->values(), + ]; + + // return view('debug', ['data' => $response]); + return response($response); + } +} diff --git a/app/Http/Controllers/WebsiteController.php b/app/Http/Controllers/WebsiteController.php index 6def7ab..f05ceb5 100644 --- a/app/Http/Controllers/WebsiteController.php +++ b/app/Http/Controllers/WebsiteController.php @@ -95,6 +95,7 @@ public function store(Request $request) 'robots_enabled' => 'boolean', 'dns_enabled' => 'boolean', 'uptime_enabled' => 'boolean', + 'uptime_keyword' => 'required_if:uptime_enabled,1' ]); $this->panel->beforeSave(function ($data) use ($request) { @@ -148,6 +149,7 @@ public function update(Request $request, Website $website) 'robots_enabled' => 'boolean', 'dns_enabled' => 'boolean', 'uptime_enabled' => 'boolean', + 'uptime_keyword' => 'required_if:uptime_enabled,1' ]); $this->panel->setEntry($website); diff --git a/app/Jobs/UptimeCheck.php b/app/Jobs/UptimeCheck.php new file mode 100644 index 0000000..649c33b --- /dev/null +++ b/app/Jobs/UptimeCheck.php @@ -0,0 +1,42 @@ +website = $website; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $checker = new Uptime($this->website); + $checker->run(); + } +} diff --git a/app/UptimeScan.php b/app/UptimeScan.php new file mode 100644 index 0000000..b055214 --- /dev/null +++ b/app/UptimeScan.php @@ -0,0 +1,25 @@ + 'boolean', + ]; + + public function getOfflineAttribute() + { + return !$this->was_online; + } + + public function getOnlineAttribute() + { + return $this->was_online; + } + +} diff --git a/app/Website.php b/app/Website.php index d37f667..7140ce8 100644 --- a/app/Website.php +++ b/app/Website.php @@ -6,11 +6,15 @@ class Website extends Model { + use HasUptime; + use HasRobots; + protected $fillable = [ 'url', 'user_id', 'ssl_enabled', 'uptime_enabled', + 'uptime_keyword', 'robots_enabled', 'dns_enabled', ]; @@ -20,31 +24,6 @@ public function user() return $this->belongsTo(User::class); } - public function robots() - { - return $this->hasMany(RobotScan::class); - } - - public function getLastRobotScansAttribute() - { - return $this->robots()->orderBy('created_at', 'desc')->take(2)->get(); - - if ($last->isEmpty()) { - return collect(); - } - - if ($last->count() === 1) { - return collect([ - $last->first(), - ]); - } - - return collect([ - $last->first(), - $last->last(), - ]); - } - public function getEditLinkAttribute() { return route('websites.edit', $this->id); @@ -55,11 +34,6 @@ public function getShowLinkAttribute() return route('websites.show', $this->id); } - public function getRobotsUrlAttribute() - { - return $this->url . '/robots.txt'; - } - public function setUrlAttribute($value) { $parts = parse_url($value); diff --git a/composer.json b/composer.json index 46b4664..6e6097b 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "sebastian/diff": "^3.0" }, "require-dev": { + "barryvdh/laravel-debugbar": "^3.2", "beyondcode/laravel-dump-server": "^1.0", "filp/whoops": "^2.0", "fzaninotto/faker": "^1.4", diff --git a/composer.lock b/composer.lock index 8eddfe6..316890c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c6758a5c7b494fa7dc8ab5cf00fb0098", + "content-hash": "ece6160ed320638076330a17011cc2bd", "packages": [ { "name": "dnoegel/php-xdg-base-dir", @@ -3341,6 +3341,74 @@ } ], "packages-dev": [ + { + "name": "barryvdh/laravel-debugbar", + "version": "v3.2.4", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-debugbar.git", + "reference": "2d195779ea4f809f69764a795e2ec371dbb76a96" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/2d195779ea4f809f69764a795e2ec371dbb76a96", + "reference": "2d195779ea4f809f69764a795e2ec371dbb76a96", + "shasum": "" + }, + "require": { + "illuminate/routing": "5.5.x|5.6.x|5.7.x|5.8.x", + "illuminate/session": "5.5.x|5.6.x|5.7.x|5.8.x", + "illuminate/support": "5.5.x|5.6.x|5.7.x|5.8.x", + "maximebf/debugbar": "~1.15.0", + "php": ">=7.0", + "symfony/debug": "^3|^4", + "symfony/finder": "^3|^4" + }, + "require-dev": { + "laravel/framework": "5.5.x" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + }, + "laravel": { + "providers": [ + "Barryvdh\\Debugbar\\ServiceProvider" + ], + "aliases": { + "Debugbar": "Barryvdh\\Debugbar\\Facade" + } + } + }, + "autoload": { + "psr-4": { + "Barryvdh\\Debugbar\\": "src/" + }, + "files": [ + "src/helpers.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "PHP Debugbar integration for Laravel", + "keywords": [ + "debug", + "debugbar", + "laravel", + "profiler", + "webprofiler" + ], + "time": "2019-03-25T09:39:08+00:00" + }, { "name": "beyondcode/laravel-dump-server", "version": "1.3.0", @@ -3617,6 +3685,67 @@ ], "time": "2016-01-20T08:20:44+00:00" }, + { + "name": "maximebf/debugbar", + "version": "v1.15.0", + "source": { + "type": "git", + "url": "https://github.com/maximebf/php-debugbar.git", + "reference": "30e7d60937ee5f1320975ca9bc7bcdd44d500f07" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/30e7d60937ee5f1320975ca9bc7bcdd44d500f07", + "reference": "30e7d60937ee5f1320975ca9bc7bcdd44d500f07", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "psr/log": "^1.0", + "symfony/var-dumper": "^2.6|^3.0|^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0|^5.0" + }, + "suggest": { + "kriswallsmith/assetic": "The best way to manage assets", + "monolog/monolog": "Log using Monolog", + "predis/predis": "Redis storage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.14-dev" + } + }, + "autoload": { + "psr-4": { + "DebugBar\\": "src/DebugBar/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maxime Bouroumeau-Fuseau", + "email": "maxime.bouroumeau@gmail.com", + "homepage": "http://maximebf.com" + }, + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "Debug bar in the browser for php application", + "homepage": "https://github.com/maximebf/php-debugbar", + "keywords": [ + "debug", + "debugbar" + ], + "time": "2017-12-15T11:13:46+00:00" + }, { "name": "mockery/mockery", "version": "1.2.3", diff --git a/database/migrations/2019_08_17_223612_create_uptime_scans_table.php b/database/migrations/2019_08_17_223612_create_uptime_scans_table.php new file mode 100644 index 0000000..6831aea --- /dev/null +++ b/database/migrations/2019_08_17_223612_create_uptime_scans_table.php @@ -0,0 +1,35 @@ +bigIncrements('id'); + $table->bigInteger('website_id'); + $table->string('response_status'); + $table->decimal('response_time'); + $table->boolean('was_online')->default(0); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('uptime_scans'); + } +} diff --git a/database/migrations/2019_08_18_064714_adding_keyword_column.php b/database/migrations/2019_08_18_064714_adding_keyword_column.php new file mode 100644 index 0000000..7e37c4e --- /dev/null +++ b/database/migrations/2019_08_18_064714_adding_keyword_column.php @@ -0,0 +1,32 @@ +string('uptime_keyword')->after('uptime_enabled')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('websites', function (Blueprint $table) { + $table->dropColumn('uptime_keyword'); + }); + } +} diff --git a/public/js/maelstrom.js b/public/js/maelstrom.js index 267ee14..8330332 100644 --- a/public/js/maelstrom.js +++ b/public/js/maelstrom.js @@ -322894,94 +322894,19 @@ function (_React$Component) { _defineProperty(_assertThisInitialized(_this), "state", { website: JSON.parse(_this.props.website), - loading: false, + loading: true, uptime: { - total: 60, - month: 95, - week: 70, - day: 20 + total: '-', + month: '-', + week: '-', + day: '-' }, - response_time: 100, - response_times: [{ - date: '2019-01-01 10:10:10', - value: 200 - }, { - date: '2019-01-02 10:10:10', - value: 100 - }, { - date: '2019-01-03 10:10:10', - value: 200 - }, { - date: '2019-01-04 10:10:10', - value: 150 - }, { - date: '2019-01-06 10:10:10', - value: 150 - }, { - date: '2019-01-06 10:10:10', - value: 150 - }, { - date: '2019-01-06 10:10:10', - value: 150 - }, { - date: '2019-01-07 10:10:10', - value: 150 - }, { - date: '2019-01-08 10:10:10', - value: 150 - }], + response_time: '-', + response_times: [], online: true, - online_time: '2 days', - last_incident: 'Monday 28th June - Downtime lasted 2 minutes.', - events: [{ - id: '1', - date: '2019-01-01 10:10:10', - type: 'down', - reason: 'xxx', - duration: '30 mins' - }, { - id: '2', - date: '2019-01-02 10:10:10', - type: 'up', - reason: 'xxx', - duration: '30 mins' - }, { - id: '3', - date: '2019-01-03 10:10:10', - type: 'down', - reason: 'xxx', - duration: '30 mins' - }, { - id: '4', - date: '2019-01-04 10:10:10', - type: 'down', - reason: 'xxx', - duration: '30 mins' - }, { - id: '5', - date: '2019-01-05 10:10:10', - type: 'up', - reason: 'xxx', - duration: '30 mins' - }, { - id: '6', - date: '2019-01-06 10:10:10', - type: 'down', - reason: 'xxx', - duration: '30 mins' - }, { - id: '7', - date: '2019-01-07 10:10:10', - type: 'down', - reason: 'xxx', - duration: '30 mins' - }, { - id: '8', - date: '2019-01-08 10:10:10', - type: 'down', - reason: 'xxx', - duration: '30 mins' - }] + online_time: '-', + last_incident: '-', + events: [] }); _defineProperty(_assertThisInitialized(_this), "update", @@ -323004,7 +322929,7 @@ function (_React$Component) { }); case 3: - endpoint = window.location.href + '/robots'; + endpoint = window.location.href + '/uptime'; if (refresh) { endpoint += '?refresh=1'; @@ -323072,7 +322997,8 @@ function (_React$Component) { _createClass(UptimeReport, [{ key: "componentDidMount", - value: function componentDidMount() {// this.update(); + value: function componentDidMount() { + this.update(); } }, { key: "renderUptime", @@ -323119,8 +323045,8 @@ function (_React$Component) { }], yAxes: [{ ticks: { - suggestedMax: Math.max.apply(Math, _toConsumableArray(data)) + 50, - suggestedMin: Math.min.apply(Math, _toConsumableArray(data)) - 50 + suggestedMax: Math.max.apply(Math, _toConsumableArray(data)) + 1, + suggestedMin: 0 } }] } diff --git a/resources/js/components/UptimeReport.js b/resources/js/components/UptimeReport.js index f670549..54b41ec 100644 --- a/resources/js/components/UptimeReport.js +++ b/resources/js/components/UptimeReport.js @@ -11,42 +11,23 @@ export default class UptimeReport extends React.Component { state = { website: JSON.parse(this.props.website), - loading: false, + loading: true, uptime: { - total: 60, - month: 95, - week: 70, - day: 20, + total: '-', + month: '-', + week: '-', + day: '-', }, - response_time: 100, - response_times: [ - { date: '2019-01-01 10:10:10', value: 200 }, - { date: '2019-01-02 10:10:10', value: 100 }, - { date: '2019-01-03 10:10:10', value: 200 }, - { date: '2019-01-04 10:10:10', value: 150 }, - { date: '2019-01-06 10:10:10', value: 150 }, - { date: '2019-01-06 10:10:10', value: 150 }, - { date: '2019-01-06 10:10:10', value: 150 }, - { date: '2019-01-07 10:10:10', value: 150 }, - { date: '2019-01-08 10:10:10', value: 150 }, - ], + response_time: '-', + response_times: [], online: true, - online_time: '2 days', - last_incident: 'Monday 28th June - Downtime lasted 2 minutes.', - events: [ - { id: '1', date: '2019-01-01 10:10:10', type: 'down', reason: 'xxx', duration: '30 mins' }, - { id: '2', date: '2019-01-02 10:10:10', type: 'up', reason: 'xxx', duration: '30 mins' }, - { id: '3', date: '2019-01-03 10:10:10', type: 'down', reason: 'xxx', duration: '30 mins' }, - { id: '4', date: '2019-01-04 10:10:10', type: 'down', reason: 'xxx', duration: '30 mins' }, - { id: '5', date: '2019-01-05 10:10:10', type: 'up', reason: 'xxx', duration: '30 mins' }, - { id: '6', date: '2019-01-06 10:10:10', type: 'down', reason: 'xxx', duration: '30 mins' }, - { id: '7', date: '2019-01-07 10:10:10', type: 'down', reason: 'xxx', duration: '30 mins' }, - { id: '8', date: '2019-01-08 10:10:10', type: 'down', reason: 'xxx', duration: '30 mins' }, - ], + online_time: '-', + last_incident: '-', + events: [], }; componentDidMount() { - // this.update(); + this.update(); } update = async(refresh = false) => { @@ -54,7 +35,7 @@ export default class UptimeReport extends React.Component { loading: true }); - let endpoint = window.location.href + '/robots'; + let endpoint = window.location.href + '/uptime'; if (refresh) { endpoint += '?refresh=1'; @@ -110,8 +91,8 @@ export default class UptimeReport extends React.Component { scales: { xAxes: [{ display: false }], yAxes: [{ ticks: { - suggestedMax: (Math.max(...data)) + 50, - suggestedMin: (Math.min(...data)) - 50 + suggestedMax: (Math.max(...data)) + 1, + suggestedMin: 0 }}], }, }} diff --git a/resources/views/debug.blade.php b/resources/views/debug.blade.php new file mode 100644 index 0000000..dc11a95 --- /dev/null +++ b/resources/views/debug.blade.php @@ -0,0 +1 @@ +@dump($data) diff --git a/resources/views/websites-form.blade.php b/resources/views/websites-form.blade.php index 5099047..abdc67a 100644 --- a/resources/views/websites-form.blade.php +++ b/resources/views/websites-form.blade.php @@ -23,7 +23,8 @@ @include('maelstrom::inputs.switch', [ 'name' => 'uptime_enabled', - 'label' => 'Enable Up-Time Monitoring?' + 'label' => 'Enable Up-Time Monitoring?', + 'hide_off' => ['uptime_keyword'], ]) @include('maelstrom::inputs.switch', [ @@ -37,6 +38,14 @@ ]) + @include('maelstrom::inputs.text', [ + 'name' => 'uptime_keyword', + 'label' => 'Uptime Keyword', + 'help' => 'This word *must* exist on the web page to confirm the site is online.', + 'prefix' => '🔑', + //'required' => true, + ]) + @endcomponent @endsection diff --git a/routes/web.php b/routes/web.php index 9c93898..bce93e5 100644 --- a/routes/web.php +++ b/routes/web.php @@ -20,4 +20,5 @@ Route::resource('websites', 'WebsiteController'); Route::get('websites/{website}/robots', 'RobotCompareController'); + Route::get('websites/{website}/uptime', 'UptimeReportController'); }); diff --git a/storage/debugbar/.gitignore b/storage/debugbar/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/debugbar/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore