Skip to content

Commit

Permalink
feat: add DummyCheckJob and related functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
janyksteenbeek committed Jan 2, 2025
1 parent 447cc68 commit c3f4845
Show file tree
Hide file tree
Showing 15 changed files with 657 additions and 82 deletions.
3 changes: 3 additions & 0 deletions app/Enums/Monitors/MonitorType.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Enums\Monitors;

use App\Jobs\Checks\DummyCheckJob;
use App\Jobs\Checks\HttpCheckJob;
use App\Jobs\Checks\TcpCheckJob;
use Filament\Support\Contracts\HasIcon;
Expand All @@ -11,12 +12,14 @@ enum MonitorType: string implements HasIcon, HasLabel
{
case HTTP = 'http';
case TCP = 'tcp';
case DUMMY = 'dummy';

public function toCheckJob(): string
{
return match ($this) {
self::HTTP => HttpCheckJob::class,
self::TCP => TcpCheckJob::class,
self::DUMMY => DummyCheckJob::class,
};
}

Expand Down
124 changes: 63 additions & 61 deletions app/Jobs/Checks/CheckJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,92 +20,94 @@ abstract class CheckJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

protected float $startTime;
protected float $endTime;

public function __construct(protected Monitor $monitor)
{
}

public function handle(): void
{
$startTime = microtime(true);
$this->startTime = microtime(true);

try {
$result = $this->performCheck();
$endTime = microtime(true);

$checkStatus = $result['status'] ?? Status::FAIL;
$responseTime = $this->calculateResponseTime($startTime, $endTime);
$responseCode = $result['response_code'] ?? null;
$output = $result['output'] ?? null;

DB::transaction(function () use ($checkStatus, $responseTime, $responseCode, $output) {
// Create the check record
$check = new Check([
'status' => $checkStatus,
'response_time' => $responseTime,
'response_code' => $responseCode,
'output' => $output,
'checked_at' => now(),
]);

$this->monitor->checks()->save($check);

// Count recent checks with the same status
$recentChecks = $this->monitor->checks()
->latest('checked_at')
->take($this->monitor->consecutive_threshold)
->get();

// Only update status if we have enough consecutive checks with the same status
if ($recentChecks->count() >= $this->monitor->consecutive_threshold &&
$recentChecks->every(fn($check) => $check->status === $checkStatus)) {
$this->monitor->update(['status' => $checkStatus]);
}
});

} catch (ConnectionException $exception) {
$this->fail($exception, $startTime);
} catch (Exception $exception) {
$this->fail($exception, $startTime);

Log::error('Failed to perform monitor check ' . $this->monitor->id . ': ' . $exception->getMessage());
Sentry::captureException($exception);
$this->endTime = microtime(true);

$this->processResult($result);
} catch (ConnectionException|Exception $exception) {
$this->handleException($exception);
}

$this->monitor->updateNextCheck();
}

abstract protected function performCheck(): array;

protected function calculateResponseTime(float $startTime, float $endTime): float
protected function processResult(array $result): void
{
return ($endTime - $startTime) * 1000; // Convert to milliseconds
DB::transaction(function () use ($result) {
$check = $this->createCheck($result);
$this->updateMonitorStatus($check->status);
});
}

protected function fail(Exception $exception, $startTime): void
protected function handleException(Exception $exception): void
{
$endTime = microtime(true);
$this->endTime = microtime(true);

DB::transaction(function () use ($startTime, $endTime, $exception) {
// Create the check record
$check = new Check([
if (!$exception instanceof ConnectionException) {
Log::error("Failed to perform monitor check {$this->monitor->id}: {$exception->getMessage()}");
Sentry::captureException($exception);
}

$check = $this->createCheck([
'status' => Status::FAIL,
'response_time' => $this->calculateResponseTime($startTime, $endTime),
'response_code' => null,
'output' => $exception->getMessage(),
'checked_at' => now(),
]);

$this->monitor->checks()->save($check);
$this->updateMonitorStatus($check->status);
}

// Count recent failures
$recentChecks = $this->monitor->checks()
->latest('checked_at')
->take($this->monitor->consecutive_threshold)
->get();
protected function createCheck(array $result): Check
{

$check = new Check([
'status' => $result['status'] ?? Status::FAIL,
'response_time' => $this->calculateResponseTime(),
'response_code' => $result['response_code'] ?? null,
'output' => $result['output'] ?? null,
'checked_at' => now(),
]);

$this->monitor->checks()->save($check);

return $check;
}

// Only update status if we have enough consecutive failures
if ($recentChecks->count() >= $this->monitor->consecutive_threshold &&
$recentChecks->every(fn($check) => $check->status === Status::FAIL)) {
$this->monitor->update(['status' => Status::FAIL]);
protected function updateMonitorStatus(Status $newStatus): void
{
$this->monitor->refresh();
// Get the most recent checks, including the current one
$recentChecks = $this->monitor->checks()
->latest('checked_at')
->take($this->monitor->consecutive_threshold)
->get();

// Only update status if we have enough checks and they all have the same status
if ($recentChecks->count() >= $this->monitor->consecutive_threshold) {
$allSameStatus = $recentChecks->every(fn($check) => $check->status === $newStatus);

if ($allSameStatus) {
$this->monitor->status = $newStatus;
$this->monitor->save();
}
});
}
}

protected function calculateResponseTime(): float
{
return ($this->endTime - $this->startTime) * 1000; // Convert to milliseconds
}
}
27 changes: 27 additions & 0 deletions app/Jobs/Checks/DummyCheckJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace App\Jobs\Checks;

use App\Enums\Checks\Status;
use App\Models\Monitor;

class DummyCheckJob extends CheckJob
{
private Status $returnStatus;

public function __construct(Monitor $monitor, Status $returnStatus = Status::OK)
{
parent::__construct($monitor);
$this->returnStatus = $returnStatus;
}

protected function performCheck(): array
{
usleep(random_int(100000, 123456));
return [
'status' => $this->returnStatus,
'response_code' => 200,
'output' => 'test output',
];
}
}
36 changes: 21 additions & 15 deletions app/Jobs/Checks/TcpCheckJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,34 @@
namespace App\Jobs\Checks;

use App\Enums\Checks\Status;
use App\Services\TcpConnectionService;
use Exception;

class TcpCheckJob extends CheckJob
{
protected function performCheck(): array
protected TcpConnectionService $tcpService;

public function __construct($monitor, ?TcpConnectionService $tcpService = null)
{
$socket = @fsockopen(
$this->monitor->address,
$this->monitor->port ?? 80,
$errno,
$errstr,
timeout: 5
);
parent::__construct($monitor);
$this->tcpService = $tcpService ?? new TcpConnectionService();
}

if (!$socket) {
throw new Exception($errstr, $errno);
}
protected function performCheck(): array
{
try {
$socket = $this->tcpService->connect(
$this->monitor->address,
$this->monitor->port ?? 80
);

fclose($socket);
$this->tcpService->close($socket);

return [
'status' => Status::OK,
];
return [
'status' => Status::OK,
];
} catch (Exception $e) {
throw $e;
}
}
}
4 changes: 2 additions & 2 deletions app/Models/Check.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;

use Illuminate\Database\Eloquent\Factories\HasFactory;
#[ObservedBy(CheckObserver::class)]
class Check extends Model
{
use HasUlids, SoftDeletes;
use HasUlids, SoftDeletes, HasFactory;

protected $guarded = [];

Expand Down
3 changes: 2 additions & 1 deletion app/Models/Monitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
Expand All @@ -20,7 +21,7 @@
#[ObservedBy(UserIdObserver::class)]
class Monitor extends Model
{
use HasUlids;
use HasUlids, HasFactory;

protected $guarded = [];

Expand Down
50 changes: 50 additions & 0 deletions app/Services/TcpConnectionService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace App\Services;

use Exception;

class TcpConnectionService
{
/**
* Open a TCP connection to the given host and port
*
* @param string $hostname
* @param int $port
* @param int $timeout
* @return resource
* @throws Exception
*/
public function connect(string $hostname, int $port, int $timeout = 5)
{
$errno = 0;
$errstr = '';

$socket = @fsockopen(
$hostname,
$port,
$errno,
$errstr,
timeout: $timeout
);

if (!$socket) {
throw new Exception($errstr, $errno);
}

return $socket;
}

/**
* Close a TCP connection
*
* @param resource $socket
* @return void
*/
public function close($socket): void
{
if (is_resource($socket)) {
fclose($socket);
}
}
}
29 changes: 29 additions & 0 deletions database/factories/CheckFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Database\Factories;

use App\Enums\Checks\Status;
use App\Models\Monitor;
use Illuminate\Database\Eloquent\Factories\Factory;
use Symfony\Component\Uid\Ulid;

/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Check>
*/
class CheckFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'id' => Ulid::generate(),
'status' => Status::OK,
'checked_at' => now(),
'monitor_id' => Monitor::factory(),
];
}
}
4 changes: 3 additions & 1 deletion database/factories/MonitorFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Models\Monitor;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Symfony\Component\Uid\Ulid;

class MonitorFactory extends Factory
{
Expand All @@ -15,14 +16,15 @@ class MonitorFactory extends Factory
public function definition(): array
{
return [

'name' => $this->faker->words(3, true),
'type' => $this->faker->randomElement(MonitorType::cases()),
'address' => $this->faker->url,
'port' => $this->faker->numberBetween(80, 9000),
'interval' => $this->faker->randomElement([1, 5, 15, 30, 60]),
'consecutive_threshold' => $this->faker->randomElement([1, 2, 3]),
'is_enabled' => true,
'status' => Status::OK,
'status' => Status::UNKNOWN,
'user_id' => User::factory(),
'next_check_at' => now(),
];
Expand Down
Loading

0 comments on commit c3f4845

Please sign in to comment.