Skip to content
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@

EasyMonitor is a full-stack monitoring platform for your websites and APIs. Add a URL, pick an interval, and get alerted when something breaks. Group monitors into projects, share live status with your users via public status pages, and run probes in multiple regions to eliminate false positives.

## Screenshot

<!-- Replace public/img/dashboard.png with your own screenshot. -->
![EasyMonitor dashboard](public/img/dashboard.png)

---

## Features
Expand Down
33 changes: 33 additions & 0 deletions app/Enums/CheckType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace App\Enums;

enum CheckType: string
{
case Http = 'http';
case Icmp = 'icmp';

/**
* Human-readable label.
*/
public function label(): string
{
return match ($this) {
self::Http => 'HTTP / HTTPS',
self::Icmp => 'Ping (ICMP)',
};
}

/**
* Short label for compact UI badges.
*/
public function shortLabel(): string
{
return match ($this) {
self::Http => 'HTTP',
self::Icmp => 'ICMP',
};
}
}
24 changes: 23 additions & 1 deletion app/Livewire/Monitors/Create.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

namespace App\Livewire\Monitors;

use App\Enums\CheckType;
use App\Models\Monitor;
use App\Models\Project;
use App\Models\Team;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Url;
use Livewire\Component;

Expand All @@ -20,6 +22,8 @@ class Create extends Component

public string $name = '';

public string $checkType = 'http';

public string $url = '';

public int $checkInterval = 60;
Expand Down Expand Up @@ -54,11 +58,16 @@ public function mount(): void
*/
public function rules(): array
{
$urlRule = $this->checkType === CheckType::Icmp->value
? ['required', 'string', 'max:255', 'regex:/^(?!.*:\/\/)[a-zA-Z0-9](?:[a-zA-Z0-9.\-:]*[a-zA-Z0-9])?$/']
: ['required', 'url', 'max:255'];

return [
'teamId' => ['nullable', 'exists:teams,id'],
'projectId' => ['nullable', 'exists:projects,id'],
'name' => ['required', 'string', 'max:255'],
'url' => ['required', 'url', 'max:255'],
'checkType' => ['required', Rule::in([CheckType::Http->value, CheckType::Icmp->value])],
'url' => $urlRule,
'checkInterval' => ['required', 'integer', 'min:30', 'max:3600'],
'isActive' => ['boolean'],
'failureThreshold' => ['required', 'integer', 'min:1', 'max:10'],
Expand All @@ -67,6 +76,18 @@ public function rules(): array
];
}

/**
* Custom validation messages
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'url.regex' => __('Enter a valid hostname or IP address (no scheme, no path).'),
];
}

/**
* Save the new monitor
*/
Expand Down Expand Up @@ -99,6 +120,7 @@ public function save(): void
'team_id' => $validated['teamId'],
'project_id' => $validated['projectId'],
'name' => $validated['name'],
'check_type' => $validated['checkType'],
'url' => $validated['url'],
'check_interval' => $validated['checkInterval'],
'is_active' => $validated['isActive'],
Expand Down
25 changes: 24 additions & 1 deletion app/Livewire/Monitors/Edit.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

namespace App\Livewire\Monitors;

use App\Enums\CheckType;
use App\Models\Monitor;
use App\Models\Project;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Validation\Rule;
use Livewire\Component;

class Edit extends Component
Expand All @@ -17,6 +19,8 @@ class Edit extends Component

public string $name = '';

public string $checkType = 'http';

public string $url = '';

public int $checkInterval = 60;
Expand All @@ -40,6 +44,7 @@ public function mount(Monitor $monitor): void
$this->monitor = $monitor;
$this->projectId = $monitor->project_id;
$this->name = $monitor->name;
$this->checkType = $monitor->check_type->value;
$this->url = $monitor->url;
$this->checkInterval = $monitor->check_interval;
$this->isActive = $monitor->is_active;
Expand All @@ -56,10 +61,15 @@ public function mount(Monitor $monitor): void
*/
public function rules(): array
{
$urlRule = $this->checkType === CheckType::Icmp->value
? ['required', 'string', 'max:255', 'regex:/^(?!.*:\/\/)[a-zA-Z0-9](?:[a-zA-Z0-9.\-:]*[a-zA-Z0-9])?$/']
: ['required', 'url', 'max:255'];

return [
'projectId' => ['nullable', 'exists:projects,id'],
'name' => ['required', 'string', 'max:255'],
'url' => ['required', 'url', 'max:255'],
'checkType' => ['required', Rule::in([CheckType::Http->value, CheckType::Icmp->value])],
'url' => $urlRule,
'checkInterval' => ['required', 'integer', 'min:30', 'max:3600'],
'isActive' => ['boolean'],
'failureThreshold' => ['required', 'integer', 'min:1', 'max:10'],
Expand All @@ -68,6 +78,18 @@ public function rules(): array
];
}

/**
* Custom validation messages
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'url.regex' => __('Enter a valid hostname or IP address (no scheme, no path).'),
];
}

/**
* Save the updated monitor
*/
Expand All @@ -89,6 +111,7 @@ public function save(): void
'project_id' => $validated['projectId'],
'team_id' => $validated['projectId'] ? null : $this->monitor->team_id,
'name' => $validated['name'],
'check_type' => $validated['checkType'],
'url' => $validated['url'],
'check_interval' => $validated['checkInterval'],
'is_active' => $validated['isActive'],
Expand Down
3 changes: 3 additions & 0 deletions app/Models/Monitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Models;

use App\Enums\CheckType;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
Expand All @@ -25,6 +26,7 @@ class Monitor extends Model
'project_id',
'name',
'url',
'check_type',
'is_active',
'status',
'check_interval',
Expand All @@ -47,6 +49,7 @@ protected function casts(): array
'is_active' => 'boolean',
'last_checked_at' => 'datetime',
'next_run_at' => 'datetime',
'check_type' => CheckType::class,
];
}

Expand Down
9 changes: 8 additions & 1 deletion app/Services/MonitoringEngine/CheckDispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace App\Services\MonitoringEngine;

use App\Enums\CheckType;
use App\Models\Monitor;
use Illuminate\Support\Facades\Redis;

Expand Down Expand Up @@ -71,13 +72,19 @@ public function dispatchCheck(Monitor $monitor): string
// so ResultConsumer can apply quorum across probes.
$roundId = (string) \Illuminate\Support\Str::uuid();

// For ICMP monitors, the URL column stores a bare host. The probe node
// discriminates check types by URL scheme, so we prefix icmp://.
$url = $monitor->check_type === CheckType::Icmp
? 'icmp://'.$monitor->url
: $monitor->url;

// XADD checks * check_id=42 url=... timeout=... round_id=...
$entryId = Redis::connection('streams')->xadd(
self::STREAM_CHECKS,
'*', // Auto-generate ID
[
'check_id' => (string) $monitor->id,
'url' => $monitor->url,
'url' => $url,
'timeout' => (string) ($monitor->check_interval * 1000), // milliseconds
'round_id' => $roundId,
]
Expand Down
12 changes: 12 additions & 0 deletions database/factories/MonitorFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public function definition(): array
'team_id' => null,
'name' => fake()->domainName(),
'url' => fake()->url(),
'check_type' => \App\Enums\CheckType::Http,
'is_active' => true,
'status' => 'pending',
'check_interval' => 60,
Expand Down Expand Up @@ -75,4 +76,15 @@ public function inactive(): static
'is_active' => false,
]);
}

/**
* Indicate that the monitor is an ICMP/ping check
*/
public function icmp(): static
{
return $this->state(fn (array $attributes) => [
'check_type' => \App\Enums\CheckType::Icmp,
'url' => fake()->domainName(),
]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('monitors', function (Blueprint $table) {
$table->string('check_type')->default('http')->after('url');
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('monitors', function (Blueprint $table) {
$table->dropColumn('check_type');
});
}
};
Binary file added public/img/dashboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 30 additions & 3 deletions resources/views/livewire/monitors/create.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,28 @@
<div class="card-body gap-5">
<h3 class="text-sm font-semibold uppercase tracking-wider text-base-content/50">{{ __('What to monitor') }}</h3>

<div class="form-control">
<label class="label pb-1">
<span class="label-text font-medium">{{ __('Check Type') }}</span>
</label>
<div class="grid grid-cols-2 gap-2">
<label class="cursor-pointer border border-base-300 rounded-lg p-3 flex items-start gap-3 hover:border-primary {{ $checkType === 'http' ? 'border-primary bg-primary/5' : '' }}">
<input type="radio" wire:model.live="checkType" value="http" class="radio radio-primary radio-sm mt-0.5" />
<div>
<div class="font-medium text-sm">{{ __('HTTP / HTTPS') }}</div>
<div class="text-xs text-base-content/60 mt-0.5">{{ __('Check a website or API endpoint') }}</div>
</div>
</label>
<label class="cursor-pointer border border-base-300 rounded-lg p-3 flex items-start gap-3 hover:border-primary {{ $checkType === 'icmp' ? 'border-primary bg-primary/5' : '' }}">
<input type="radio" wire:model.live="checkType" value="icmp" class="radio radio-primary radio-sm mt-0.5" />
<div>
<div class="font-medium text-sm">{{ __('Ping (ICMP)') }}</div>
<div class="text-xs text-base-content/60 mt-0.5">{{ __('Check that a host is reachable') }}</div>
</div>
</label>
</div>
</div>

<div class="form-control">
<label class="label pb-1">
<span class="label-text font-medium">{{ __('Display Name') }}</span>
Expand All @@ -43,15 +65,20 @@ class="input input-bordered w-full rounded-lg @error('name') input-error @enderr

<div class="form-control">
<label class="label pb-1">
<span class="label-text font-medium">{{ __('URL') }}</span>
<span class="label-text font-medium">{{ $checkType === 'icmp' ? __('Host') : __('URL') }}</span>
</label>
<input
type="url"
type="text"
wire:model="url"
required
class="input input-bordered w-full rounded-lg @error('url') input-error @enderror"
placeholder="https://example.com"
placeholder="{{ $checkType === 'icmp' ? '1.1.1.1' : 'https://example.com' }}"
/>
<div class="label pb-0">
<span class="label-text-alt text-base-content/50">
{{ $checkType === 'icmp' ? __('Hostname or IP address — no scheme or path.') : __('Full URL including https://') }}
</span>
</div>
@error('url')
<div class="label pb-0">
<span class="label-text-alt text-error">{{ $message }}</span>
Expand Down
33 changes: 30 additions & 3 deletions resources/views/livewire/monitors/edit.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,28 @@
<div class="card-body gap-5">
<h3 class="text-sm font-semibold uppercase tracking-wider text-base-content/50">{{ __('What to monitor') }}</h3>

<div class="form-control">
<label class="label pb-1">
<span class="label-text font-medium">{{ __('Check Type') }}</span>
</label>
<div class="grid grid-cols-2 gap-2">
<label class="cursor-pointer border border-base-300 rounded-lg p-3 flex items-start gap-3 hover:border-primary {{ $checkType === 'http' ? 'border-primary bg-primary/5' : '' }}">
<input type="radio" wire:model.live="checkType" value="http" class="radio radio-primary radio-sm mt-0.5" />
<div>
<div class="font-medium text-sm">{{ __('HTTP / HTTPS') }}</div>
<div class="text-xs text-base-content/60 mt-0.5">{{ __('Check a website or API endpoint') }}</div>
</div>
</label>
<label class="cursor-pointer border border-base-300 rounded-lg p-3 flex items-start gap-3 hover:border-primary {{ $checkType === 'icmp' ? 'border-primary bg-primary/5' : '' }}">
<input type="radio" wire:model.live="checkType" value="icmp" class="radio radio-primary radio-sm mt-0.5" />
<div>
<div class="font-medium text-sm">{{ __('Ping (ICMP)') }}</div>
<div class="text-xs text-base-content/60 mt-0.5">{{ __('Check that a host is reachable') }}</div>
</div>
</label>
</div>
</div>

<div class="form-control">
<label class="label pb-1">
<span class="label-text font-medium">{{ __('Display Name') }}</span>
Expand All @@ -40,15 +62,20 @@ class="input input-bordered w-full rounded-lg @error('name') input-error @enderr

<div class="form-control">
<label class="label pb-1">
<span class="label-text font-medium">{{ __('URL') }}</span>
<span class="label-text font-medium">{{ $checkType === 'icmp' ? __('Host') : __('URL') }}</span>
</label>
<input
type="url"
type="text"
wire:model="url"
required
class="input input-bordered w-full rounded-lg @error('url') input-error @enderror"
placeholder="https://example.com"
placeholder="{{ $checkType === 'icmp' ? '1.1.1.1' : 'https://example.com' }}"
/>
<div class="label pb-0">
<span class="label-text-alt text-base-content/50">
{{ $checkType === 'icmp' ? __('Hostname or IP address — no scheme or path.') : __('Full URL including https://') }}
</span>
</div>
@error('url')
<div class="label pb-0">
<span class="label-text-alt text-error">{{ $message }}</span>
Expand Down
Loading
Loading