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

✨ Health Checks for Links #79

Merged
merged 5 commits into from
Sep 8, 2020
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
53 changes: 53 additions & 0 deletions app/Console/Commands/LinkHealthChecks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace App\Console\Commands;

use App\Link;
use App\Services\Shaark\Shaark;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Console\Command;

class LinkHealthChecks extends Command
{
protected $signature = 'shaark:link_health_check {--all}';
protected $description = 'Run health checks on links';

public function __construct()
{
parent::__construct();
}

public function handle()
{
Link::where('is_health_check_enabled', 1)
->where(function ($query) {
return $query->where('http_checked_at', '<', now()->subDays(app(Shaark::class)->getLinkHealthChecksAge()))
->orWhereNull('http_checked_at');
})
->orderBy('http_checked_at', 'ASC')
->when(! $this->option('all'), function($query) {
return $query->limit(20);
})
->get()
->each(function (Link $link) {
try {
$response = (new Client())->request('GET', $link->getUrlAttribute(), [
'headers' => [
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36',
],
'http_errors' => false,
'timeout' => 5,
]);

$link->http_status = $response->getStatusCode();
} catch (RequestException $exception) {
// Might happen when the domain has expired
$link->http_status = 500;
} finally {
$link->http_checked_at = now();
$link->save();
}
});
}
}
14 changes: 14 additions & 0 deletions app/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ protected function schedule(Schedule $schedule)

// Make backup
$this->scheduleBackup($schedule);

// Link health checks
$this->scheduleLinkHealthChecks($schedule);
}

protected function scheduleBackup(Schedule $schedule): self
Expand All @@ -54,4 +57,15 @@ protected function scheduleBackup(Schedule $schedule): self

return $this;
}

protected function scheduleLinkHealthChecks(Schedule $schedule): self
{
$shaark = app(Shaark::class);

if (false === $shaark->getLinkHealthChecksEnabled()) {
return $this;
}

$schedule->command('shaark:link_health_check')->everyTenMinutes();
}
}
3 changes: 2 additions & 1 deletion app/Http/Controllers/Api/LinkController.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public function store(StoreLinkRequest $request)
'title',
'content',
'url',
'is_health_check_enabled',
])->toArray());

$link->updatePreview();
Expand Down Expand Up @@ -69,7 +70,7 @@ public function update(StoreLinkRequest $request, int $id)
$link = Link::findOrFail($id);
$data = collect($request->validated());

$link->fill($data->only('title', 'content', 'url')->toArray());
$link->fill($data->only('title', 'content', 'url', 'is_health_check_enabled')->toArray());
$link->updatePreview();

$link->post->is_pinned = $data->get('is_pinned', $link->post->is_pinned);
Expand Down
55 changes: 55 additions & 0 deletions app/Http/Controllers/Manage/LinksController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace App\Http\Controllers\Manage;

use App\Http\Controllers\Controller;
use App\Link;
use App\Services\HealthCheckStats;
use App\Services\Shaark\Shaark;

class LinksController extends Controller
{
public function __construct()
{
$this->middleware('auth');

$this->middleware('demo')->except('view');
}

public function view()
{
return view('manage.links')->with([
'page_title' => __('Links'),
'stats' => new HealthCheckStats(),
]);
}

public function viewDead()
{
return view('manage.links_dead')->with([
'page_title' => __('Dead Links'),
'stats' => new HealthCheckStats(),
'dead_links' => Link::whereBetween('http_status', [400, 499])->orderBy('http_checked_at', 'DESC')->paginate(10),
]);
}

public function viewOther()
{
return view('manage.links_other')->with([
'page_title' => __('Other Status Links'),
'stats' => new HealthCheckStats(),
'other_links' => Link::whereBetween('http_status', [300, 399])
->orWhereBetween('http_status', [500, 599])
->orderBy('http_checked_at', 'DESC')->paginate(10),
]);
}

public function viewDisabled()
{
return view('manage.links_disabled')->with([
'page_title' => __('Disabled Links'),
'stats' => new HealthCheckStats(),
'disabled_links' => Link::where('is_health_check_enabled', 0)->paginate(10),
]);
}
}
3 changes: 3 additions & 0 deletions app/Http/Requests/StoreLinkRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ public function rules()
'required',
'url',
],
'is_health_check_enabled' => [
'nullable',
],
'is_private' => [
'nullable',
],
Expand Down
34 changes: 34 additions & 0 deletions app/Http/Resources/LinkResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ public function toArray($request)
'title' => $this->title,
'content' => $this->content,
'url' => $this->url,
'is_health_check_enabled' => $this->is_health_check_enabled,
'http_status' => $this->getStatusText($this->http_status),
'http_status_color' => $this->getStatusColor($this->http_status),
'http_checked_at' => $this->http_checked_at,
'permalink' => $this->permalink,
'is_private' => $this->post->is_private,
'is_pinned' => $this->post->is_pinned,
Expand All @@ -33,4 +37,34 @@ public function toArray($request)
])
];
}

public function getStatusText($status)
{
if (is_null($status)) {
return null;
}

if ($status == 200) {
return 'Healthy (200)';
}

if (400 <= $status and $status <= 499) {
return 'Dead (4xx)';
}

return 'Other (3xx, 5xx)';
}

public function getStatusColor($status)
{
if ($status == 200) {
return 'success';
}

if (400 <= $status and $status <= 499) {
return 'danger';
}

return 'warning';
}
}
1 change: 1 addition & 0 deletions app/Link.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Link extends Model
'preview',
'archive',
'url',
'is_health_check_enabled',
];
protected $appends = [
'permalink',
Expand Down
79 changes: 79 additions & 0 deletions app/Services/HealthCheckStats.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace App\Services;

use App\Services\Shaark\Shaark;

class HealthCheckStats
{
/** @var \Illuminate\Support\Collection $stats */
public $stats;

public function __construct()
{
$this->stats = \DB::table('links')
->select('http_status', \DB::raw('count(id) as num_count'))
->groupBy('http_status')
->get();
}

public function get()
{
return [
'num_healthy' => $this->countHealthy(),
'num_other' => $this->countOther(),
'num_dead' => $this->countDead(),
'num_pending' => $this->countPending(),
];
}

public function isHealthCheckEnabled()
{
return app(Shaark::class)->getLinkHealthChecksEnabled();
}

public function countTotal()
{
return $this->stats->sum('num_count');
}

public function countHealthy()
{
return $this->stats->where('http_status', 200)->sum('num_count');
}

public function countOther()
{
$redirects = $this->stats->whereBetween('http_status', [300, 399])
->sum('num_count');

$server_errors = $this->stats->whereBetween('http_status', [500, 599])
->sum('num_count');

return $redirects + $server_errors;
}

public function countDead()
{
return $this->stats->whereBetween('http_status', [400, 499])
->sum('num_count');
}

public function countPending()
{
return \DB::table('links')
->where('is_health_check_enabled', 1)
->where(function ($query) {
return $query->where('http_checked_at', '<', now()->subDays(app(Shaark::class)->getLinkHealthChecksAge()))
->orWhereNull('http_checked_at');
})
->count();
}

public function countDisabled()
{
return \DB::table('links')
->where('is_health_check_enabled', 0)
->count();
}
}
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"php": "^7.2",
"doctrine/dbal": "^2.9",
"fideloper/proxy": "^4.0",
"guzzlehttp/guzzle": "^6.5",
"hashids/hashids": "^2.0.4|~3.0",
"lab404/laravel-auth-checker": "^1.4",
"laravel/framework": "^6.18",
Expand Down
Loading