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

Add optional pings to horizon and schedule checks #248

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
18 changes: 18 additions & 0 deletions config/health.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,24 @@
'url' => '/oh-dear-health-check-results',
],

/*
* You can specify a heartbeat URL for the Horizon check.
* This URL will be pinged if the Horizon check is successful.
* This way you can get notified if Horizon goes down.
*/
'horizon' => [
'heartbeat_url' => env('HORIZON_HEARTBEAT_URL', null),
],

/*
* You can specify a heartbeat URL for the Schedule check.
* This URL will be pinged if the Schedule check is successful.
* This way you can get notified if the schedule fails to run.
*/
'schedule' => [
'heartbeat_url' => env('SCHEDULE_HEARTBEAT_URL', null),
],

/*
* You can set a theme for the local results page
*
Expand Down
34 changes: 33 additions & 1 deletion docs/available-checks/horizon.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Horizon
weight: 13
---

This check will make sure Horizon is running. It will report a warning when Horizon is paused, and a failure when Horizon is not running.
This check will make sure Horizon is running. It will report a warning when Horizon is paused, and a failure when Horizon is not running.

## Usage

Expand All @@ -17,3 +17,35 @@ Health::checks([
HorizonCheck::new(),
]);
```

## Ping Configuration

The check can be configured to ping a URL when Horizon is running successfully. This is useful for external monitoring services like Oh Dear, Pingdom, Envoyer heartbeats etc.

If a URL is configured, it will automatically be pinged each time the health checks run via the `RunHealthChecksCommand` in your scheduler at the frequency you've configured. NB!!! The URL will only be pinged if the check passes.

The ping is independent of the check's status, so the check may pass but the ping may fail (e.g. the ping URL is malformed or unreachable).

### Simple Setup

The easiest way to set up pinging is through your `.env` file:

```env
HORIZON_HEARTBEAT_URL=https://your-monitoring-service.com/ping/abc123
```

When this URL is set, it will automatically be pinged each time the health checks run via the `RunHealthChecksCommand` in your scheduler at the frequency you've configured.

### Advanced Configuration

For more control, you can set the timeout and retry times in your check registration:

```php
Health::checks([
HorizonCheck::new()
->pingTimeout(5) // Set timeout in seconds (default: 3)
->pingRetryTimes(3) // Set number of retry attempts (default: 1)
]);
```

When a ping fails, it will be logged to your application's log file.
32 changes: 32 additions & 0 deletions docs/available-checks/schedule.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,35 @@ ScheduleCheck::new()->heartbeatMaxAgeInMinutes(2),
### Checking individual scheduled tasks

To monitor your individual scheduled tasks, take a look at [our schedule monitor package](https://github.com/spatie/laravel-schedule-monitor).

## Ping Configuration

The check can be configured to ping a URL when the scheduler is running successfully. This is useful for external monitoring services like Oh Dear, Pingdom, Envoyer heartbeats etc.

If a URL is configured, it will automatically be pinged each time the health checks run via the `ScheduleCheckHeartbeatCommand` in your scheduler at the frequency you've configured. NB!!! The URL will only be pinged if the check passes.

The ping is independent of the check's status, so the check may pass but the ping may fail (e.g. the ping URL is malformed or unreachable).

### Simple Setup

The easiest way to set up pinging is through your `.env` file:

```env
SCHEDULE_HEARTBEAT_URL=https://your-monitoring-service.com/ping/abc123
```

When this URL is set, it will automatically be pinged each time the health checks run via the `ScheduleCheckHeartbeatCommand` in your scheduler at the frequency you've configured.

### Advanced Configuration

For more control, you can set the timeout and retry times in your check registration:

```php
Health::checks([
ScheduleCheck::new()
->pingTimeout(5) // Set timeout in seconds (default: 3)
->pingRetryTimes(3) // Set number of retry attempts (default: 1)
]);
```

When a ping fails, it will be logged to your application's log file.
7 changes: 7 additions & 0 deletions src/Checks/Checks/HorizonCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
use Laravel\Horizon\Contracts\MasterSupervisorRepository;
use Spatie\Health\Checks\Check;
use Spatie\Health\Checks\Result;
use Spatie\Health\Traits\Pingable;

class HorizonCheck extends Check
{
use Pingable;

public function run(): Result
{
$result = Result::make();
Expand All @@ -35,6 +38,10 @@ public function run(): Result
->shortSummary('Paused');
}

if (config('health.horizon.heartbeat_url')) {
$this->pingUrl(config('health.horizon.heartbeat_url'));
}

return $result->ok()->shortSummary('Running');
}
}
14 changes: 12 additions & 2 deletions src/Checks/Checks/ScheduleCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
use Composer\InstalledVersions;
use Spatie\Health\Checks\Check;
use Spatie\Health\Checks\Result;
use Spatie\Health\Traits\Pingable;

class ScheduleCheck extends Check
{
use Pingable;

protected string $cacheKey = 'health:checks:schedule:latestHeartbeatAt';

protected ?string $cacheStoreName = null;
Expand Down Expand Up @@ -62,15 +65,22 @@ public function run(): Result

$minutesAgo = $latestHeartbeatAt->diffInMinutes();

if (version_compare($carbonVersion,
'3.0.0', '<')) {
if (version_compare(
$carbonVersion,
'3.0.0',
'<'
)) {
$minutesAgo += 1;
}

if ($minutesAgo > $this->heartbeatMaxAgeInMinutes) {
return $result->failed("The last run of the schedule was more than {$minutesAgo} minutes ago.");
}

if (config('health.schedule.heartbeat_url')) {
$this->pingUrl(config('health.schedule.heartbeat_url'));
}

return $result;
}
}
59 changes: 59 additions & 0 deletions src/Traits/Pingable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace Spatie\Health\Traits;

use InvalidArgumentException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

trait Pingable
{
protected int $timeout = 3; // seconds
protected int $retryTimes = 1;

protected function pingUrl(?string $url = null): void
{
if (! $url || empty($url)) {
return;
}

if (! $this->isValidUrl($url)) {
Log::error("Invalid URL provided for health check ping: {$url}");
return;
}

try {
Http::timeout($this->timeout)
->retry($this->retryTimes)
->get($url);
} catch (\Exception $e) {
Log::error('Failed to ping health check URL: ' . $e->getMessage());
}
}

protected function isValidUrl(string $url): bool
{
if (! filter_var($url, FILTER_VALIDATE_URL)) {
return false;
}

return true;
}

public function pingTimeout(int $seconds): self
{
if ($seconds <= 0) {
throw new InvalidArgumentException('Timeout must be a positive integer.');
}
$this->timeout = $seconds;

return $this;
}

public function pingRetryTimes(int $times): self
{
$this->retryTimes = $times;

return $this;
}
}
24 changes: 24 additions & 0 deletions tests/Checks/HorizonCheckTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Spatie\Health\Checks\Checks\HorizonCheck;
use Spatie\Health\Enums\Status;
use Illuminate\Support\Facades\Http;

it('will fail when horizon is not running', function () {
$result = HorizonCheck::new()->run();
Expand All @@ -30,3 +31,26 @@

expect($result)->status->toBe(Status::ok());
});

it('pings heartbeat url when configured', function () {
Http::fake();
config()->set('health.horizon.heartbeat_url', 'https://example.com/heartbeat');

$this->fakeHorizonStatus('running');

HorizonCheck::new()->run();

Http::assertSent(function ($request) {
return $request->url() === 'https://example.com/heartbeat';
});
});

it('does not ping heartbeat url when not configured', function () {
Http::fake();
config()->set('health.horizon.heartbeat_url', null);

$this->fakeHorizonStatus('running');

HorizonCheck::new()->run();
Http::assertNothingSent();
});
27 changes: 27 additions & 0 deletions tests/Checks/ScheduleCheckTest.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php

use Illuminate\Support\Facades\Http;
use Spatie\Health\Checks\Checks\ScheduleCheck;
use Spatie\Health\Commands\ScheduleCheckHeartbeatCommand;
use Spatie\Health\Enums\Status;
Expand Down Expand Up @@ -49,3 +50,29 @@
$result = $this->scheduleCheck->run();
expect($result->status)->toBe(Status::failed());
});

it('pings heartbeat url when configured', function () {
Http::fake();
config()->set('health.schedule.heartbeat_url', 'https://example.com/heartbeat');

artisan(ScheduleCheckHeartbeatCommand::class)->assertSuccessful();

$result = $this->scheduleCheck->run();
expect($result->status)->toBe(Status::ok());

Http::assertSent(function ($request) {
return $request->url() === 'https://example.com/heartbeat';
});
});

it('does not ping heartbeat url when not configured', function () {
Http::fake();
config()->set('health.schedule.heartbeat_url', null);

artisan(ScheduleCheckHeartbeatCommand::class)->assertSuccessful();

$result = $this->scheduleCheck->run();
expect($result->status)->toBe(Status::ok());

Http::assertNothingSent();
});
Loading