some progress
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\CheckDnsRecord;
|
||||
use App\Models\DnsRecord;
|
||||
use App\Models\Site;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class MonitorDns extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'monitor:dns
|
||||
{--dns-record= : Specific DNS record ID to monitor}
|
||||
{--site= : Monitor all DNS records for a specific site ID}
|
||||
{--sync : Run checks synchronously instead of queuing}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Monitor DNS records for all sites';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('Starting DNS monitoring...');
|
||||
|
||||
$query = DnsRecord::with('site')->where('is_active', true);
|
||||
|
||||
// Filter by specific DNS record
|
||||
if ($dnsRecordId = $this->option('dns-record')) {
|
||||
$query->where('id', $dnsRecordId);
|
||||
}
|
||||
|
||||
// Filter by site
|
||||
if ($siteId = $this->option('site')) {
|
||||
$query->where('site_id', $siteId);
|
||||
}
|
||||
|
||||
$dnsRecords = $query->get();
|
||||
|
||||
if ($dnsRecords->isEmpty()) {
|
||||
$this->warn('No active DNS records found.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info("Found {$dnsRecords->count()} active DNS record(s).");
|
||||
|
||||
$bar = $this->output->createProgressBar($dnsRecords->count());
|
||||
$bar->start();
|
||||
|
||||
$dispatched = 0;
|
||||
$errors = 0;
|
||||
|
||||
foreach ($dnsRecords as $dnsRecord) {
|
||||
try {
|
||||
if ($this->option('sync')) {
|
||||
// Run synchronously
|
||||
CheckDnsRecord::dispatchSync($dnsRecord);
|
||||
$this->newLine();
|
||||
$this->line(" ✓ Checked: {$dnsRecord->site->name} - {$dnsRecord->record_type} {$dnsRecord->domain}");
|
||||
} else {
|
||||
// Queue the job
|
||||
CheckDnsRecord::dispatch($dnsRecord);
|
||||
}
|
||||
|
||||
$dispatched++;
|
||||
} catch (\Exception $e) {
|
||||
$errors++;
|
||||
$this->newLine();
|
||||
$this->error(" ✗ Error checking {$dnsRecord->domain}: {$e->getMessage()}");
|
||||
}
|
||||
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine(2);
|
||||
|
||||
// Summary
|
||||
$this->info("DNS monitoring completed!");
|
||||
$this->table(
|
||||
['Metric', 'Count'],
|
||||
[
|
||||
['Total DNS Records', $dnsRecords->count()],
|
||||
['Successfully Dispatched', $dispatched],
|
||||
['Errors', $errors],
|
||||
]
|
||||
);
|
||||
|
||||
if (!$this->option('sync')) {
|
||||
$this->comment('Jobs have been queued. Run the queue worker to process them.');
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\CheckHealthEndpoint;
|
||||
use App\Models\HealthCheck;
|
||||
use App\Models\Site;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class MonitorHealth extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'monitor:health
|
||||
{--health-check= : Specific health check ID to monitor}
|
||||
{--site= : Monitor all health checks for a specific site ID}
|
||||
{--sync : Run checks synchronously instead of queuing}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Monitor application health endpoints';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('Starting health endpoint monitoring...');
|
||||
|
||||
$query = HealthCheck::with('site')->where('is_active', true);
|
||||
|
||||
// Filter by specific health check
|
||||
if ($healthCheckId = $this->option('health-check')) {
|
||||
$query->where('id', $healthCheckId);
|
||||
}
|
||||
|
||||
// Filter by site
|
||||
if ($siteId = $this->option('site')) {
|
||||
$query->where('site_id', $siteId);
|
||||
}
|
||||
|
||||
$healthChecks = $query->get();
|
||||
|
||||
if ($healthChecks->isEmpty()) {
|
||||
$this->warn('No active health checks found.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info("Found {$healthChecks->count()} active health check(s).");
|
||||
|
||||
$bar = $this->output->createProgressBar($healthChecks->count());
|
||||
$bar->start();
|
||||
|
||||
$dispatched = 0;
|
||||
$errors = 0;
|
||||
|
||||
foreach ($healthChecks as $healthCheck) {
|
||||
try {
|
||||
if ($this->option('sync')) {
|
||||
// Run synchronously
|
||||
CheckHealthEndpoint::dispatchSync($healthCheck);
|
||||
$this->newLine();
|
||||
$this->line(" ✓ Checked: {$healthCheck->site->name} - {$healthCheck->endpoint}");
|
||||
} else {
|
||||
// Queue the job
|
||||
CheckHealthEndpoint::dispatch($healthCheck);
|
||||
}
|
||||
|
||||
$dispatched++;
|
||||
} catch (\Exception $e) {
|
||||
$errors++;
|
||||
$this->newLine();
|
||||
$this->error(" ✗ Error checking {$healthCheck->site->name} - {$healthCheck->endpoint}: {$e->getMessage()}");
|
||||
}
|
||||
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine(2);
|
||||
|
||||
// Summary
|
||||
$this->info("Health monitoring completed!");
|
||||
$this->table(
|
||||
['Metric', 'Count'],
|
||||
[
|
||||
['Total Health Checks', $healthChecks->count()],
|
||||
['Successfully Dispatched', $dispatched],
|
||||
['Errors', $errors],
|
||||
]
|
||||
);
|
||||
|
||||
if (!$this->option('sync')) {
|
||||
$this->comment('Jobs have been queued. Run the queue worker to process them.');
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\CheckSitePing;
|
||||
use App\Models\Site;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class MonitorPing extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'monitor:ping
|
||||
{--site= : Specific site ID to ping}
|
||||
{--sync : Run synchronously instead of dispatching jobs}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Ping all enabled sites to check their uptime status';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('Starting site ping monitoring...');
|
||||
|
||||
$query = Site::where('monitoring_enabled', true);
|
||||
|
||||
if ($siteId = $this->option('site')) {
|
||||
$query->where('id', $siteId);
|
||||
}
|
||||
|
||||
$sites = $query->get();
|
||||
|
||||
if ($sites->isEmpty()) {
|
||||
$this->warn('No enabled sites found to monitor.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info("Found {$sites->count()} site(s) to ping.");
|
||||
|
||||
$progressBar = $this->output->createProgressBar($sites->count());
|
||||
$progressBar->start();
|
||||
|
||||
$dispatched = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($sites as $site) {
|
||||
try {
|
||||
if ($this->option('sync')) {
|
||||
// Run synchronously for testing/debugging
|
||||
CheckSitePing::dispatchSync($site);
|
||||
} else {
|
||||
// Dispatch to queue
|
||||
CheckSitePing::dispatch($site);
|
||||
}
|
||||
|
||||
$dispatched++;
|
||||
} catch (\Exception $e) {
|
||||
$failed++;
|
||||
Log::error('MonitorPing: Failed to dispatch job', [
|
||||
'site_id' => $site->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
$progressBar->advance();
|
||||
}
|
||||
|
||||
$progressBar->finish();
|
||||
$this->newLine(2);
|
||||
|
||||
$this->info("Successfully dispatched {$dispatched} ping job(s).");
|
||||
|
||||
if ($failed > 0) {
|
||||
$this->error("Failed to dispatch {$failed} job(s).");
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\CheckTcpPort;
|
||||
use App\Models\TcpPort;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class MonitorTcpPorts extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'monitor:tcp-ports
|
||||
{--tcp-port= : Specific TCP port ID to check}
|
||||
{--site= : Specific site ID to check all ports for}
|
||||
{--sync : Run synchronously instead of dispatching jobs}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Monitor all active TCP ports';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('Starting TCP port monitoring...');
|
||||
|
||||
$query = TcpPort::with('site')->where('is_active', true);
|
||||
|
||||
if ($tcpPortId = $this->option('tcp-port')) {
|
||||
$query->where('id', $tcpPortId);
|
||||
}
|
||||
|
||||
if ($siteId = $this->option('site')) {
|
||||
$query->where('site_id', $siteId);
|
||||
}
|
||||
|
||||
$tcpPorts = $query->get();
|
||||
|
||||
if ($tcpPorts->isEmpty()) {
|
||||
$this->warn('No active TCP ports found to monitor.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info("Found {$tcpPorts->count()} TCP port(s) to check.");
|
||||
|
||||
$progressBar = $this->output->createProgressBar($tcpPorts->count());
|
||||
$progressBar->start();
|
||||
|
||||
$dispatched = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($tcpPorts as $tcpPort) {
|
||||
try {
|
||||
if ($this->option('sync')) {
|
||||
// Run synchronously for testing/debugging
|
||||
CheckTcpPort::dispatchSync($tcpPort);
|
||||
} else {
|
||||
// Dispatch to queue
|
||||
CheckTcpPort::dispatch($tcpPort);
|
||||
}
|
||||
|
||||
$dispatched++;
|
||||
} catch (\Exception $e) {
|
||||
$failed++;
|
||||
Log::error('MonitorTcpPorts: Failed to dispatch job', [
|
||||
'tcp_port_id' => $tcpPort->id,
|
||||
'site_id' => $tcpPort->site_id,
|
||||
'port' => $tcpPort->port,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
$progressBar->advance();
|
||||
}
|
||||
|
||||
$progressBar->finish();
|
||||
$this->newLine(2);
|
||||
|
||||
$this->info("Successfully dispatched {$dispatched} TCP port check job(s).");
|
||||
|
||||
if ($failed > 0) {
|
||||
$this->error("Failed to dispatch {$failed} job(s).");
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new API token for the authenticated user.
|
||||
*/
|
||||
public function createToken(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'token_name' => ['required', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
$token = $request->user()->createToken($validated['token_name']);
|
||||
|
||||
return response()->json([
|
||||
'token' => $token->plainTextToken,
|
||||
'type' => 'Bearer',
|
||||
], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all tokens for the authenticated user.
|
||||
*/
|
||||
public function tokens(Request $request): JsonResponse
|
||||
{
|
||||
$tokens = $request->user()->tokens()->get(['id', 'name', 'last_used_at', 'created_at']);
|
||||
|
||||
return response()->json([
|
||||
'data' => $tokens,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke a specific token.
|
||||
*/
|
||||
public function revokeToken(Request $request, string $tokenId): JsonResponse
|
||||
{
|
||||
$deleted = $request->user()->tokens()->where('id', $tokenId)->delete();
|
||||
|
||||
if (!$deleted) {
|
||||
return response()->json([
|
||||
'message' => 'Token not found',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Token revoked successfully',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke all tokens for the authenticated user.
|
||||
*/
|
||||
public function revokeAllTokens(Request $request): JsonResponse
|
||||
{
|
||||
$request->user()->tokens()->delete();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'All tokens revoked successfully',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Api\BrokenLinksIndexRequest;
|
||||
use App\Models\Site;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LinkController extends Controller
|
||||
{
|
||||
/**
|
||||
* Get broken links list for a site.
|
||||
*/
|
||||
public function index(BrokenLinksIndexRequest $request, Site $site): JsonResponse
|
||||
{
|
||||
// Ensure the site belongs to the authenticated user
|
||||
if ($site->user_id !== $request->user()->id) {
|
||||
return response()->json([
|
||||
'message' => 'Unauthorized',
|
||||
], 403);
|
||||
}
|
||||
|
||||
$perPage = $request->input('per_page', 50);
|
||||
$perPage = min($perPage, 100); // Max 100 per page
|
||||
|
||||
$brokenLinks = $site->brokenLinks()
|
||||
->orderBy('last_checked_at', 'desc')
|
||||
->paginate($perPage);
|
||||
|
||||
return response()->json($brokenLinks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get broken links statistics for a site.
|
||||
*/
|
||||
public function stats(Request $request, Site $site): JsonResponse
|
||||
{
|
||||
// Ensure the site belongs to the authenticated user
|
||||
if ($site->user_id !== $request->user()->id) {
|
||||
return response()->json([
|
||||
'message' => 'Unauthorized',
|
||||
], 403);
|
||||
}
|
||||
|
||||
$totalBrokenLinks = $site->brokenLinks()->count();
|
||||
|
||||
$statusCodeBreakdown = $site->brokenLinks()
|
||||
->selectRaw('status_code, COUNT(*) as count')
|
||||
->groupBy('status_code')
|
||||
->orderBy('count', 'desc')
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'total_broken_links' => $totalBrokenLinks,
|
||||
'status_code_breakdown' => $statusCodeBreakdown,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Site;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SiteController extends Controller
|
||||
{
|
||||
/**
|
||||
* List all sites for the authenticated user.
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$sites = $request->user()
|
||||
->sites()
|
||||
->withCount(['uptimeChecks', 'sslCertificates', 'brokenLinks'])
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'data' => $sites,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific site for the authenticated user.
|
||||
*/
|
||||
public function show(Request $request, Site $site): JsonResponse
|
||||
{
|
||||
// Ensure the site belongs to the authenticated user
|
||||
if ($site->user_id !== $request->user()->id) {
|
||||
return response()->json([
|
||||
'message' => 'Unauthorized',
|
||||
], 403);
|
||||
}
|
||||
|
||||
$site->loadCount(['uptimeChecks', 'sslCertificates', 'brokenLinks']);
|
||||
|
||||
return response()->json([
|
||||
'data' => $site,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Api\SslHistoryRequest;
|
||||
use App\Models\Site;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SslController extends Controller
|
||||
{
|
||||
/**
|
||||
* Get SSL certificate data for a site.
|
||||
*/
|
||||
public function show(Request $request, Site $site): JsonResponse
|
||||
{
|
||||
// Ensure the site belongs to the authenticated user
|
||||
if ($site->user_id !== $request->user()->id) {
|
||||
return response()->json([
|
||||
'message' => 'Unauthorized',
|
||||
], 403);
|
||||
}
|
||||
|
||||
$latestCertificate = $site->sslCertificates()
|
||||
->latest('checked_at')
|
||||
->first();
|
||||
|
||||
if (!$latestCertificate) {
|
||||
return response()->json([
|
||||
'message' => 'No SSL certificate data found',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$daysUntilExpiry = null;
|
||||
$isExpiring = false;
|
||||
|
||||
if ($latestCertificate->expires_at) {
|
||||
$daysUntilExpiry = now()->diffInDays($latestCertificate->expires_at, false);
|
||||
$isExpiring = $daysUntilExpiry <= 30 && $daysUntilExpiry > 0;
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'issuer' => $latestCertificate->issuer,
|
||||
'expires_at' => $latestCertificate->expires_at,
|
||||
'checked_at' => $latestCertificate->checked_at,
|
||||
'days_until_expiry' => $daysUntilExpiry ? (int) $daysUntilExpiry : null,
|
||||
'is_expiring_soon' => $isExpiring,
|
||||
'is_expired' => $daysUntilExpiry !== null && $daysUntilExpiry < 0,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SSL certificate history for a site.
|
||||
*/
|
||||
public function history(SslHistoryRequest $request, Site $site): JsonResponse
|
||||
{
|
||||
// Ensure the site belongs to the authenticated user
|
||||
if ($site->user_id !== $request->user()->id) {
|
||||
return response()->json([
|
||||
'message' => 'Unauthorized',
|
||||
], 403);
|
||||
}
|
||||
|
||||
$perPage = $request->input('per_page', 20);
|
||||
$perPage = min($perPage, 100); // Max 100 per page
|
||||
|
||||
$certificates = $site->sslCertificates()
|
||||
->orderBy('checked_at', 'desc')
|
||||
->paginate($perPage);
|
||||
|
||||
return response()->json($certificates);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Api\UptimeIndexRequest;
|
||||
use App\Models\Site;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UptimeController extends Controller
|
||||
{
|
||||
/**
|
||||
* Get uptime history for a site (paginated).
|
||||
*/
|
||||
public function index(UptimeIndexRequest $request, Site $site): JsonResponse
|
||||
{
|
||||
// Ensure the site belongs to the authenticated user
|
||||
if ($site->user_id !== $request->user()->id) {
|
||||
return response()->json([
|
||||
'message' => 'Unauthorized',
|
||||
], 403);
|
||||
}
|
||||
|
||||
$perPage = $request->input('per_page', 50);
|
||||
$perPage = min($perPage, 100); // Max 100 per page
|
||||
|
||||
$uptimeChecks = $site->uptimeChecks()
|
||||
->orderBy('checked_at', 'desc')
|
||||
->paginate($perPage);
|
||||
|
||||
return response()->json($uptimeChecks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get uptime statistics for a site.
|
||||
*/
|
||||
public function stats(Request $request, Site $site): JsonResponse
|
||||
{
|
||||
// Ensure the site belongs to the authenticated user
|
||||
if ($site->user_id !== $request->user()->id) {
|
||||
return response()->json([
|
||||
'message' => 'Unauthorized',
|
||||
], 403);
|
||||
}
|
||||
|
||||
$totalChecks = $site->uptimeChecks()->count();
|
||||
$successfulChecks = $site->uptimeChecks()
|
||||
->whereBetween('status_code', [200, 299])
|
||||
->count();
|
||||
|
||||
$uptimePercentage = $totalChecks > 0
|
||||
? round(($successfulChecks / $totalChecks) * 100, 2)
|
||||
: 0;
|
||||
|
||||
$averageResponseTime = $site->uptimeChecks()
|
||||
->avg('response_time_ms');
|
||||
|
||||
$latestCheck = $site->uptimeChecks()
|
||||
->latest('checked_at')
|
||||
->first();
|
||||
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'total_checks' => $totalChecks,
|
||||
'successful_checks' => $successfulChecks,
|
||||
'uptime_percentage' => $uptimePercentage,
|
||||
'average_response_time_ms' => $averageResponseTime ? round($averageResponseTime, 2) : null,
|
||||
'latest_check' => $latestCheck,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\DnsRecord;
|
||||
use App\Models\Site;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DnsRecordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the DNS records for a site.
|
||||
*/
|
||||
public function index(Site $site): View
|
||||
{
|
||||
// Ensure the user owns the site
|
||||
if ($site->user_id !== Auth::id()) {
|
||||
abort(403, 'Unauthorized access to this site.');
|
||||
}
|
||||
|
||||
$dnsRecords = $site->dnsRecords()
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
return view('dns-records.index', compact('site', 'dnsRecords'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new DNS record.
|
||||
*/
|
||||
public function create(Site $site): View
|
||||
{
|
||||
// Ensure the user owns the site
|
||||
if ($site->user_id !== Auth::id()) {
|
||||
abort(403, 'Unauthorized access to this site.');
|
||||
}
|
||||
|
||||
$recordTypes = DnsRecord::getRecordTypes();
|
||||
|
||||
return view('dns-records.create', compact('site', 'recordTypes'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created DNS record in storage.
|
||||
*/
|
||||
public function store(Request $request, Site $site): RedirectResponse
|
||||
{
|
||||
// Ensure the user owns the site
|
||||
if ($site->user_id !== Auth::id()) {
|
||||
abort(403, 'Unauthorized access to this site.');
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'domain' => ['required', 'string', 'max:255'],
|
||||
'record_type' => ['required', 'string', 'in:A,AAAA,CNAME,MX,TXT,NS,SOA,PTR,SRV'],
|
||||
'expected_value' => ['nullable', 'string', 'max:1000'],
|
||||
'is_active' => ['boolean'],
|
||||
]);
|
||||
|
||||
// Check for duplicate record
|
||||
$exists = $site->dnsRecords()
|
||||
->where('domain', $validated['domain'])
|
||||
->where('record_type', $validated['record_type'])
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
return back()
|
||||
->withInput()
|
||||
->withErrors(['domain' => "A {$validated['record_type']} record for {$validated['domain']} already exists for this site."]);
|
||||
}
|
||||
|
||||
$dnsRecord = $site->dnsRecords()->create([
|
||||
'domain' => $validated['domain'],
|
||||
'record_type' => $validated['record_type'],
|
||||
'expected_value' => $validated['expected_value'] ?? null,
|
||||
'is_active' => $validated['is_active'] ?? true,
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('dns-records.show', [$site, $dnsRecord])
|
||||
->with('success', 'DNS record created successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified DNS record.
|
||||
*/
|
||||
public function show(Site $site, DnsRecord $dnsRecord): View
|
||||
{
|
||||
// Ensure the user owns the site and the DNS record belongs to the site
|
||||
if ($site->user_id !== Auth::id() || $dnsRecord->site_id !== $site->id) {
|
||||
abort(403, 'Unauthorized access to this DNS record.');
|
||||
}
|
||||
|
||||
return view('dns-records.show', compact('site', 'dnsRecord'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified DNS record.
|
||||
*/
|
||||
public function edit(Site $site, DnsRecord $dnsRecord): View
|
||||
{
|
||||
// Ensure the user owns the site and the DNS record belongs to the site
|
||||
if ($site->user_id !== Auth::id() || $dnsRecord->site_id !== $site->id) {
|
||||
abort(403, 'Unauthorized access to this DNS record.');
|
||||
}
|
||||
|
||||
$recordTypes = DnsRecord::getRecordTypes();
|
||||
|
||||
return view('dns-records.edit', compact('site', 'dnsRecord', 'recordTypes'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified DNS record in storage.
|
||||
*/
|
||||
public function update(Request $request, Site $site, DnsRecord $dnsRecord): RedirectResponse
|
||||
{
|
||||
// Ensure the user owns the site and the DNS record belongs to the site
|
||||
if ($site->user_id !== Auth::id() || $dnsRecord->site_id !== $site->id) {
|
||||
abort(403, 'Unauthorized access to this DNS record.');
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'domain' => ['required', 'string', 'max:255'],
|
||||
'record_type' => ['required', 'string', 'in:A,AAAA,CNAME,MX,TXT,NS,SOA,PTR,SRV'],
|
||||
'expected_value' => ['nullable', 'string', 'max:1000'],
|
||||
'is_active' => ['boolean'],
|
||||
]);
|
||||
|
||||
// Check for duplicate record (excluding current record)
|
||||
$exists = $site->dnsRecords()
|
||||
->where('domain', $validated['domain'])
|
||||
->where('record_type', $validated['record_type'])
|
||||
->where('id', '!=', $dnsRecord->id)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
return back()
|
||||
->withInput()
|
||||
->withErrors(['domain' => "A {$validated['record_type']} record for {$validated['domain']} already exists for this site."]);
|
||||
}
|
||||
|
||||
$dnsRecord->update([
|
||||
'domain' => $validated['domain'],
|
||||
'record_type' => $validated['record_type'],
|
||||
'expected_value' => $validated['expected_value'] ?? null,
|
||||
'is_active' => $validated['is_active'] ?? $dnsRecord->is_active,
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('dns-records.show', [$site, $dnsRecord])
|
||||
->with('success', 'DNS record updated successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified DNS record from storage.
|
||||
*/
|
||||
public function destroy(Site $site, DnsRecord $dnsRecord): RedirectResponse
|
||||
{
|
||||
// Ensure the user owns the site and the DNS record belongs to the site
|
||||
if ($site->user_id !== Auth::id() || $dnsRecord->site_id !== $site->id) {
|
||||
abort(403, 'Unauthorized access to this DNS record.');
|
||||
}
|
||||
|
||||
$dnsRecord->delete();
|
||||
|
||||
return redirect()
|
||||
->route('dns-records.index', $site)
|
||||
->with('success', 'DNS record deleted successfully.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\HealthCheck;
|
||||
use App\Models\Site;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class HealthCheckController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the health checks for a site.
|
||||
*/
|
||||
public function index(Site $site): View
|
||||
{
|
||||
// Ensure the user owns the site
|
||||
if ($site->user_id !== Auth::id()) {
|
||||
abort(403, 'Unauthorized access to this site.');
|
||||
}
|
||||
|
||||
$healthChecks = $site->healthChecks()
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
return view('health-checks.index', compact('site', 'healthChecks'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new health check.
|
||||
*/
|
||||
public function create(Site $site): View
|
||||
{
|
||||
// Ensure the user owns the site
|
||||
if ($site->user_id !== Auth::id()) {
|
||||
abort(403, 'Unauthorized access to this site.');
|
||||
}
|
||||
|
||||
return view('health-checks.create', compact('site'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created health check in storage.
|
||||
*/
|
||||
public function store(Request $request, Site $site): RedirectResponse
|
||||
{
|
||||
// Ensure the user owns the site
|
||||
if ($site->user_id !== Auth::id()) {
|
||||
abort(403, 'Unauthorized access to this site.');
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'endpoint' => ['required', 'string', 'max:255'],
|
||||
'expected_json_keys' => ['nullable', 'string'],
|
||||
'performance_threshold' => ['nullable', 'integer', 'min:1', 'max:60000'],
|
||||
'is_active' => ['boolean'],
|
||||
]);
|
||||
|
||||
// Convert comma-separated keys to array
|
||||
$expectedKeys = null;
|
||||
if (!empty($validated['expected_json_keys'])) {
|
||||
$expectedKeys = array_map(
|
||||
'trim',
|
||||
explode(',', $validated['expected_json_keys'])
|
||||
);
|
||||
}
|
||||
|
||||
// Check for duplicate endpoint
|
||||
$exists = $site->healthChecks()
|
||||
->where('endpoint', $validated['endpoint'])
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
return back()
|
||||
->withInput()
|
||||
->withErrors(['endpoint' => 'This endpoint is already being monitored for this site.']);
|
||||
}
|
||||
|
||||
$healthCheck = $site->healthChecks()->create([
|
||||
'endpoint' => $validated['endpoint'],
|
||||
'expected_json_keys' => $expectedKeys,
|
||||
'performance_threshold' => $validated['performance_threshold'] ?? null,
|
||||
'is_active' => $validated['is_active'] ?? true,
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('health-checks.show', [$site, $healthCheck])
|
||||
->with('success', 'Health check created successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified health check.
|
||||
*/
|
||||
public function show(Site $site, HealthCheck $healthCheck): View
|
||||
{
|
||||
// Ensure the user owns the site and the health check belongs to the site
|
||||
if ($site->user_id !== Auth::id() || $healthCheck->site_id !== $site->id) {
|
||||
abort(403, 'Unauthorized access to this health check.');
|
||||
}
|
||||
|
||||
return view('health-checks.show', compact('site', 'healthCheck'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified health check.
|
||||
*/
|
||||
public function edit(Site $site, HealthCheck $healthCheck): View
|
||||
{
|
||||
// Ensure the user owns the site and the health check belongs to the site
|
||||
if ($site->user_id !== Auth::id() || $healthCheck->site_id !== $site->id) {
|
||||
abort(403, 'Unauthorized access to this health check.');
|
||||
}
|
||||
|
||||
return view('health-checks.edit', compact('site', 'healthCheck'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified health check in storage.
|
||||
*/
|
||||
public function update(Request $request, Site $site, HealthCheck $healthCheck): RedirectResponse
|
||||
{
|
||||
// Ensure the user owns the site and the health check belongs to the site
|
||||
if ($site->user_id !== Auth::id() || $healthCheck->site_id !== $site->id) {
|
||||
abort(403, 'Unauthorized access to this health check.');
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'endpoint' => ['required', 'string', 'max:255'],
|
||||
'expected_json_keys' => ['nullable', 'string'],
|
||||
'performance_threshold' => ['nullable', 'integer', 'min:1', 'max:60000'],
|
||||
'is_active' => ['boolean'],
|
||||
]);
|
||||
|
||||
// Convert comma-separated keys to array
|
||||
$expectedKeys = null;
|
||||
if (!empty($validated['expected_json_keys'])) {
|
||||
$expectedKeys = array_map(
|
||||
'trim',
|
||||
explode(',', $validated['expected_json_keys'])
|
||||
);
|
||||
}
|
||||
|
||||
// Check for duplicate endpoint (excluding current health check)
|
||||
$exists = $site->healthChecks()
|
||||
->where('endpoint', $validated['endpoint'])
|
||||
->where('id', '!=', $healthCheck->id)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
return back()
|
||||
->withInput()
|
||||
->withErrors(['endpoint' => 'This endpoint is already being monitored for this site.']);
|
||||
}
|
||||
|
||||
$healthCheck->update([
|
||||
'endpoint' => $validated['endpoint'],
|
||||
'expected_json_keys' => $expectedKeys,
|
||||
'performance_threshold' => $validated['performance_threshold'] ?? null,
|
||||
'is_active' => $validated['is_active'] ?? $healthCheck->is_active,
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('health-checks.show', [$site, $healthCheck])
|
||||
->with('success', 'Health check updated successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified health check from storage.
|
||||
*/
|
||||
public function destroy(Site $site, HealthCheck $healthCheck): RedirectResponse
|
||||
{
|
||||
// Ensure the user owns the site and the health check belongs to the site
|
||||
if ($site->user_id !== Auth::id() || $healthCheck->site_id !== $site->id) {
|
||||
abort(403, 'Unauthorized access to this health check.');
|
||||
}
|
||||
|
||||
$healthCheck->delete();
|
||||
|
||||
return redirect()
|
||||
->route('health-checks.index', $site)
|
||||
->with('success', 'Health check deleted successfully.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Site;
|
||||
use App\Models\TcpPort;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class TcpPortController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(Request $request, Site $site)
|
||||
{
|
||||
// Ensure the site belongs to the authenticated user
|
||||
if ($site->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$tcpPorts = $site->tcpPorts()
|
||||
->orderBy('port')
|
||||
->paginate(20);
|
||||
|
||||
return view('tcp-ports.index', compact('site', 'tcpPorts'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function create(Site $site)
|
||||
{
|
||||
// Ensure the site belongs to the authenticated user
|
||||
if ($site->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$commonPorts = TcpPort::getCommonPorts();
|
||||
|
||||
return view('tcp-ports.create', compact('site', 'commonPorts'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(Request $request, Site $site)
|
||||
{
|
||||
// Ensure the site belongs to the authenticated user
|
||||
if ($site->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'port' => ['required', 'integer', 'min:1', 'max:65535'],
|
||||
'name' => ['nullable', 'string', 'max:255'],
|
||||
'is_active' => ['boolean'],
|
||||
]);
|
||||
|
||||
// Check if port already exists for this site
|
||||
$exists = $site->tcpPorts()
|
||||
->where('port', $validated['port'])
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
return back()
|
||||
->withInput()
|
||||
->withErrors(['port' => 'This port is already being monitored for this site.']);
|
||||
}
|
||||
|
||||
$site->tcpPorts()->create([
|
||||
'port' => $validated['port'],
|
||||
'name' => $validated['name'] ?? null,
|
||||
'is_active' => $validated['is_active'] ?? true,
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('tcp-ports.index', $site)
|
||||
->with('success', 'TCP port added successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Site $site, TcpPort $tcpPort)
|
||||
{
|
||||
// Ensure the port belongs to the site and user
|
||||
if ($tcpPort->site_id !== $site->id || $site->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
return view('tcp-ports.show', compact('site', 'tcpPort'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit(Site $site, TcpPort $tcpPort)
|
||||
{
|
||||
// Ensure the port belongs to the site and user
|
||||
if ($tcpPort->site_id !== $site->id || $site->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$commonPorts = TcpPort::getCommonPorts();
|
||||
|
||||
return view('tcp-ports.edit', compact('site', 'tcpPort', 'commonPorts'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, Site $site, TcpPort $tcpPort)
|
||||
{
|
||||
// Ensure the port belongs to the site and user
|
||||
if ($tcpPort->site_id !== $site->id || $site->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'port' => ['required', 'integer', 'min:1', 'max:65535'],
|
||||
'name' => ['nullable', 'string', 'max:255'],
|
||||
'is_active' => ['boolean'],
|
||||
]);
|
||||
|
||||
// Check if port already exists for this site (excluding current)
|
||||
$exists = $site->tcpPorts()
|
||||
->where('port', $validated['port'])
|
||||
->where('id', '!=', $tcpPort->id)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
return back()
|
||||
->withInput()
|
||||
->withErrors(['port' => 'This port is already being monitored for this site.']);
|
||||
}
|
||||
|
||||
$tcpPort->update([
|
||||
'port' => $validated['port'],
|
||||
'name' => $validated['name'] ?? null,
|
||||
'is_active' => $validated['is_active'] ?? $tcpPort->is_active,
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('tcp-ports.index', $site)
|
||||
->with('success', 'TCP port updated successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Site $site, TcpPort $tcpPort)
|
||||
{
|
||||
// Ensure the port belongs to the site and user
|
||||
if ($tcpPort->site_id !== $site->id || $site->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$tcpPort->delete();
|
||||
|
||||
return redirect()
|
||||
->route('tcp-ports.index', $site)
|
||||
->with('success', 'TCP port deleted successfully.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class BrokenLinksIndexRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class SslHistoryRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UptimeIndexRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\DnsRecord;
|
||||
use App\Notifications\DnsRecordAlert;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CheckDnsRecord implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* The number of times the job may be attempted.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $tries = 3;
|
||||
|
||||
/**
|
||||
* The number of seconds the job can run before timing out.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $timeout = 30;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public DnsRecord $dnsRecord
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
Log::info('CheckDnsRecord: Starting check', [
|
||||
'dns_record_id' => $this->dnsRecord->id,
|
||||
'domain' => $this->dnsRecord->domain,
|
||||
'record_type' => $this->dnsRecord->record_type,
|
||||
]);
|
||||
|
||||
// Perform DNS lookup
|
||||
$result = $this->performDnsLookup();
|
||||
|
||||
$wasValid = $this->dnsRecord->is_valid;
|
||||
$isValid = $result['is_valid'];
|
||||
$actualValue = $result['actual_value'];
|
||||
$errorMessage = $result['error_message'];
|
||||
|
||||
// Update DNS record
|
||||
$this->dnsRecord->update([
|
||||
'last_checked_at' => now(),
|
||||
'is_valid' => $isValid,
|
||||
'actual_value' => $actualValue,
|
||||
'error_message' => $errorMessage,
|
||||
]);
|
||||
|
||||
Log::info('CheckDnsRecord: Check completed', [
|
||||
'dns_record_id' => $this->dnsRecord->id,
|
||||
'is_valid' => $isValid,
|
||||
'actual_value' => $actualValue,
|
||||
]);
|
||||
|
||||
// Send notification if status changed from valid to invalid
|
||||
if ($wasValid === true && $isValid === false) {
|
||||
$this->dnsRecord->site->user->notify(new DnsRecordAlert(
|
||||
$this->dnsRecord,
|
||||
'invalid',
|
||||
$actualValue,
|
||||
$errorMessage
|
||||
));
|
||||
|
||||
Log::warning('CheckDnsRecord: Invalid alert sent', [
|
||||
'dns_record_id' => $this->dnsRecord->id,
|
||||
'error_message' => $errorMessage,
|
||||
]);
|
||||
}
|
||||
|
||||
// Send notification when DNS is restored
|
||||
if ($wasValid === false && $isValid === true) {
|
||||
$this->dnsRecord->site->user->notify(new DnsRecordAlert(
|
||||
$this->dnsRecord,
|
||||
'valid',
|
||||
$actualValue
|
||||
));
|
||||
|
||||
Log::info('CheckDnsRecord: Valid alert sent', [
|
||||
'dns_record_id' => $this->dnsRecord->id,
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('CheckDnsRecord: Unexpected error', [
|
||||
'dns_record_id' => $this->dnsRecord->id,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
$this->dnsRecord->update([
|
||||
'last_checked_at' => now(),
|
||||
'is_valid' => false,
|
||||
'error_message' => 'Unexpected error: ' . $e->getMessage(),
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform DNS lookup and validation.
|
||||
*/
|
||||
protected function performDnsLookup(): array
|
||||
{
|
||||
try {
|
||||
// Map record type to DNS_* constant
|
||||
$dnsType = $this->getDnsTypeConstant($this->dnsRecord->record_type);
|
||||
|
||||
if ($dnsType === null) {
|
||||
return [
|
||||
'is_valid' => false,
|
||||
'actual_value' => null,
|
||||
'error_message' => "Unsupported DNS record type: {$this->dnsRecord->record_type}",
|
||||
];
|
||||
}
|
||||
|
||||
// Perform DNS lookup
|
||||
$records = @dns_get_record($this->dnsRecord->domain, $dnsType);
|
||||
|
||||
if ($records === false) {
|
||||
$error = error_get_last();
|
||||
return [
|
||||
'is_valid' => false,
|
||||
'actual_value' => null,
|
||||
'error_message' => 'DNS lookup failed: ' . ($error['message'] ?? 'Unknown error'),
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($records)) {
|
||||
return [
|
||||
'is_valid' => false,
|
||||
'actual_value' => null,
|
||||
'error_message' => "No {$this->dnsRecord->record_type} records found for {$this->dnsRecord->domain}",
|
||||
];
|
||||
}
|
||||
|
||||
// Extract values based on record type
|
||||
$actualValues = $this->extractDnsValues($records, $this->dnsRecord->record_type);
|
||||
|
||||
// If no expected value, just return that records exist
|
||||
if (empty($this->dnsRecord->expected_value)) {
|
||||
return [
|
||||
'is_valid' => true,
|
||||
'actual_value' => $actualValues,
|
||||
'error_message' => null,
|
||||
];
|
||||
}
|
||||
|
||||
// Validate against expected value
|
||||
$isValid = $this->validateDnsValue($actualValues, $this->dnsRecord->expected_value);
|
||||
|
||||
return [
|
||||
'is_valid' => $isValid,
|
||||
'actual_value' => $actualValues,
|
||||
'error_message' => $isValid ? null : 'DNS record does not match expected value',
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'is_valid' => false,
|
||||
'actual_value' => null,
|
||||
'error_message' => 'DNS lookup error: ' . $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DNS type constant for dns_get_record().
|
||||
*/
|
||||
protected function getDnsTypeConstant(string $recordType): ?int
|
||||
{
|
||||
return match (strtoupper($recordType)) {
|
||||
'A' => DNS_A,
|
||||
'AAAA' => DNS_AAAA,
|
||||
'CNAME' => DNS_CNAME,
|
||||
'MX' => DNS_MX,
|
||||
'TXT' => DNS_TXT,
|
||||
'NS' => DNS_NS,
|
||||
'SOA' => DNS_SOA,
|
||||
'PTR' => DNS_PTR,
|
||||
'SRV' => DNS_SRV,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract DNS values from records.
|
||||
*/
|
||||
protected function extractDnsValues(array $records, string $recordType): array
|
||||
{
|
||||
$values = [];
|
||||
|
||||
foreach ($records as $record) {
|
||||
$value = match (strtoupper($recordType)) {
|
||||
'A' => $record['ip'] ?? null,
|
||||
'AAAA' => $record['ipv6'] ?? null,
|
||||
'CNAME' => $record['target'] ?? null,
|
||||
'MX' => isset($record['target']) ? "{$record['pri']} {$record['target']}" : null,
|
||||
'TXT' => $record['txt'] ?? null,
|
||||
'NS' => $record['target'] ?? null,
|
||||
'SOA' => isset($record['mname']) ? $record['mname'] : null,
|
||||
'PTR' => $record['target'] ?? null,
|
||||
'SRV' => isset($record['target']) ? "{$record['pri']} {$record['weight']} {$record['port']} {$record['target']}" : null,
|
||||
default => null,
|
||||
};
|
||||
|
||||
if ($value !== null) {
|
||||
$values[] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate DNS value against expected value.
|
||||
*/
|
||||
protected function validateDnsValue(array $actualValues, string $expectedValue): bool
|
||||
{
|
||||
// Normalize expected value
|
||||
$expectedValue = trim($expectedValue);
|
||||
|
||||
// Check if any actual value matches expected
|
||||
foreach ($actualValues as $actualValue) {
|
||||
// Exact match
|
||||
if (strcasecmp($actualValue, $expectedValue) === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Partial match for MX and SRV records (check target only)
|
||||
if (stripos($actualValue, $expectedValue) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a job failure.
|
||||
*/
|
||||
public function failed(\Throwable $exception): void
|
||||
{
|
||||
Log::error('CheckDnsRecord: Job failed after all retries', [
|
||||
'dns_record_id' => $this->dnsRecord->id,
|
||||
'exception' => $exception->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\HealthCheck;
|
||||
use App\Notifications\HealthCheckAlert;
|
||||
use App\Notifications\PerformanceAlert;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CheckHealthEndpoint implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* The number of times the job may be attempted.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $tries = 3;
|
||||
|
||||
/**
|
||||
* The number of seconds the job can run before timing out.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $timeout = 60;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public HealthCheck $healthCheck
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
$client = new Client([
|
||||
'timeout' => 10,
|
||||
'connect_timeout' => 5,
|
||||
'allow_redirects' => [
|
||||
'max' => 3,
|
||||
'track_redirects' => true,
|
||||
],
|
||||
'http_errors' => false,
|
||||
]);
|
||||
|
||||
$url = $this->healthCheck->full_url;
|
||||
$startTime = microtime(true);
|
||||
|
||||
Log::info('CheckHealthEndpoint: Starting check', [
|
||||
'health_check_id' => $this->healthCheck->id,
|
||||
'url' => $url,
|
||||
]);
|
||||
|
||||
$response = $client->get($url, [
|
||||
'headers' => [
|
||||
'Accept' => 'application/json',
|
||||
'User-Agent' => 'IsMySiteUp Health Monitor/1.0',
|
||||
],
|
||||
]);
|
||||
|
||||
$endTime = microtime(true);
|
||||
$responseTime = (int) round(($endTime - $startTime) * 1000);
|
||||
$statusCode = $response->getStatusCode();
|
||||
$body = (string) $response->getBody();
|
||||
|
||||
// Try to decode JSON
|
||||
$responseData = null;
|
||||
$isValidJson = false;
|
||||
|
||||
try {
|
||||
$responseData = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
|
||||
$isValidJson = true;
|
||||
} catch (\JsonException $e) {
|
||||
Log::warning('CheckHealthEndpoint: Invalid JSON response', [
|
||||
'health_check_id' => $this->healthCheck->id,
|
||||
'error' => $e->getMessage(),
|
||||
'body_preview' => substr($body, 0, 500),
|
||||
]);
|
||||
}
|
||||
|
||||
// Determine if endpoint is healthy
|
||||
$isHealthy = $this->determineHealth($statusCode, $isValidJson, $responseData);
|
||||
|
||||
$wasHealthy = $this->healthCheck->is_healthy;
|
||||
|
||||
// Update health check record
|
||||
$this->healthCheck->update([
|
||||
'last_checked_at' => now(),
|
||||
'is_healthy' => $isHealthy,
|
||||
'response_data' => $responseData,
|
||||
'response_time_ms' => $responseTime,
|
||||
'status_code' => $statusCode,
|
||||
]);
|
||||
|
||||
Log::info('CheckHealthEndpoint: Check completed', [
|
||||
'health_check_id' => $this->healthCheck->id,
|
||||
'is_healthy' => $isHealthy,
|
||||
'status_code' => $statusCode,
|
||||
'response_time_ms' => $responseTime,
|
||||
]);
|
||||
|
||||
// Send notification if health status changed from healthy to unhealthy
|
||||
if ($wasHealthy === true && $isHealthy === false) {
|
||||
$this->healthCheck->site->user->notify(new HealthCheckAlert(
|
||||
$this->healthCheck,
|
||||
'unhealthy',
|
||||
$statusCode,
|
||||
$responseTime
|
||||
));
|
||||
|
||||
Log::warning('CheckHealthEndpoint: Unhealthy alert sent', [
|
||||
'health_check_id' => $this->healthCheck->id,
|
||||
'status_code' => $statusCode,
|
||||
]);
|
||||
}
|
||||
|
||||
// Optionally send notification when health is restored
|
||||
if ($wasHealthy === false && $isHealthy === true) {
|
||||
$this->healthCheck->site->user->notify(new HealthCheckAlert(
|
||||
$this->healthCheck,
|
||||
'healthy',
|
||||
$statusCode,
|
||||
$responseTime
|
||||
));
|
||||
|
||||
Log::info('CheckHealthEndpoint: Healthy alert sent', [
|
||||
'health_check_id' => $this->healthCheck->id,
|
||||
]);
|
||||
}
|
||||
|
||||
// Check performance threshold and send alert if exceeded
|
||||
if ($this->healthCheck->performance_threshold && $responseTime > $this->healthCheck->performance_threshold) {
|
||||
$this->healthCheck->site->user->notify(new PerformanceAlert(
|
||||
resourceType: 'Health Check',
|
||||
resourceName: $this->healthCheck->endpoint,
|
||||
responseTime: $responseTime,
|
||||
threshold: $this->healthCheck->performance_threshold,
|
||||
url: $this->healthCheck->full_url,
|
||||
resourceId: $this->healthCheck->site_id
|
||||
));
|
||||
|
||||
Log::warning('CheckHealthEndpoint: Performance threshold exceeded', [
|
||||
'health_check_id' => $this->healthCheck->id,
|
||||
'response_time_ms' => $responseTime,
|
||||
'threshold_ms' => $this->healthCheck->performance_threshold,
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (ConnectException $e) {
|
||||
// Connection timeout or DNS failure
|
||||
Log::error('CheckHealthEndpoint: Connection failed', [
|
||||
'health_check_id' => $this->healthCheck->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
$this->healthCheck->update([
|
||||
'last_checked_at' => now(),
|
||||
'is_healthy' => false,
|
||||
'response_data' => ['error' => 'Connection failed: ' . $e->getMessage()],
|
||||
'response_time_ms' => null,
|
||||
'status_code' => null,
|
||||
]);
|
||||
|
||||
} catch (RequestException $e) {
|
||||
// HTTP error occurred
|
||||
$statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : null;
|
||||
|
||||
Log::error('CheckHealthEndpoint: Request failed', [
|
||||
'health_check_id' => $this->healthCheck->id,
|
||||
'status_code' => $statusCode,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
$this->healthCheck->update([
|
||||
'last_checked_at' => now(),
|
||||
'is_healthy' => false,
|
||||
'response_data' => ['error' => 'Request failed: ' . $e->getMessage()],
|
||||
'response_time_ms' => null,
|
||||
'status_code' => $statusCode,
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
// Any other exception
|
||||
Log::error('CheckHealthEndpoint: Unexpected error', [
|
||||
'health_check_id' => $this->healthCheck->id,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
$this->healthCheck->update([
|
||||
'last_checked_at' => now(),
|
||||
'is_healthy' => false,
|
||||
'response_data' => ['error' => 'Unexpected error: ' . $e->getMessage()],
|
||||
'response_time_ms' => null,
|
||||
'status_code' => null,
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the endpoint is healthy based on response.
|
||||
*/
|
||||
protected function determineHealth(int $statusCode, bool $isValidJson, ?array $responseData): bool
|
||||
{
|
||||
// Check if status code is successful (2xx)
|
||||
if ($statusCode < 200 || $statusCode >= 300) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if response is valid JSON
|
||||
if (!$isValidJson) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate expected JSON keys if configured
|
||||
if (!empty($this->healthCheck->expected_json_keys)) {
|
||||
if (!$this->healthCheck->validateResponseKeys($responseData ?? [])) {
|
||||
Log::warning('CheckHealthEndpoint: Missing expected JSON keys', [
|
||||
'health_check_id' => $this->healthCheck->id,
|
||||
'expected_keys' => $this->healthCheck->expected_json_keys,
|
||||
'response_keys' => array_keys($responseData ?? []),
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a job failure.
|
||||
*/
|
||||
public function failed(\Throwable $exception): void
|
||||
{
|
||||
Log::error('CheckHealthEndpoint: Job failed after all retries', [
|
||||
'health_check_id' => $this->healthCheck->id,
|
||||
'exception' => $exception->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Site;
|
||||
use App\Models\UptimeCheck;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CheckSitePing implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* The number of times the job may be attempted.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $tries = 3;
|
||||
|
||||
/**
|
||||
* The number of seconds the job can run before timing out.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $timeout = 60;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public Site $site
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
$host = $this->extractHost($this->site->url);
|
||||
|
||||
if (!$host) {
|
||||
Log::error('CheckSitePing: Invalid URL', [
|
||||
'site_id' => $this->site->id,
|
||||
'url' => $this->site->url,
|
||||
]);
|
||||
|
||||
// Record failed check due to invalid URL
|
||||
UptimeCheck::create([
|
||||
'site_id' => $this->site->id,
|
||||
'check_type' => 'ping',
|
||||
'status_code' => null,
|
||||
'response_time_ms' => null,
|
||||
'is_online' => false,
|
||||
'checked_at' => now(),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->ping($host);
|
||||
|
||||
Log::info('CheckSitePing: Raw result', [
|
||||
'site_id' => $this->site->id,
|
||||
'host' => $host,
|
||||
'result' => $result,
|
||||
]);
|
||||
|
||||
UptimeCheck::create([
|
||||
'site_id' => $this->site->id,
|
||||
'check_type' => 'ping',
|
||||
'status_code' => null, // ICMP doesn't have HTTP status codes
|
||||
'response_time_ms' => $result['avg_time'],
|
||||
'is_online' => $result['is_online'],
|
||||
'checked_at' => now(),
|
||||
]);
|
||||
|
||||
Log::info('CheckSitePing: Ping completed', [
|
||||
'site_id' => $this->site->id,
|
||||
'host' => $host,
|
||||
'is_online' => $result['is_online'],
|
||||
'avg_time' => $result['avg_time'],
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('CheckSitePing: Failed', [
|
||||
'site_id' => $this->site->id,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
// Record failed check
|
||||
UptimeCheck::create([
|
||||
'site_id' => $this->site->id,
|
||||
'check_type' => 'ping',
|
||||
'status_code' => null,
|
||||
'response_time_ms' => null,
|
||||
'is_online' => false,
|
||||
'checked_at' => now(),
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract host from URL.
|
||||
*/
|
||||
protected function extractHost(string $url): ?string
|
||||
{
|
||||
$parsed = parse_url($url);
|
||||
return $parsed['host'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute ping command and parse results.
|
||||
*/
|
||||
protected function ping(string $host): array
|
||||
{
|
||||
$isWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
|
||||
|
||||
// Check if ping command exists
|
||||
$pingPath = $this->findPingCommand($isWindows);
|
||||
|
||||
if (!$pingPath) {
|
||||
// Ping command not found, use TCP connection as fallback
|
||||
Log::warning('CheckSitePing: ping command not found, using TCP fallback', [
|
||||
'site_id' => $this->site->id,
|
||||
'host' => $host,
|
||||
]);
|
||||
|
||||
return $this->tcpCheck($host);
|
||||
}
|
||||
|
||||
// Build ping command based on OS
|
||||
if ($isWindows) {
|
||||
$command = sprintf('%s -n 4 %s 2>&1', $pingPath, escapeshellarg($host));
|
||||
} else {
|
||||
$command = sprintf('%s -c 4 -W 5 %s 2>&1', $pingPath, escapeshellarg($host));
|
||||
}
|
||||
|
||||
$output = [];
|
||||
$returnVar = 0;
|
||||
|
||||
exec($command, $output, $returnVar);
|
||||
|
||||
$isOnline = $returnVar === 0;
|
||||
$avgTime = null;
|
||||
|
||||
// Parse average response time
|
||||
if ($isOnline) {
|
||||
$avgTime = $this->parseAverageTime($output, $isWindows);
|
||||
}
|
||||
|
||||
return [
|
||||
'is_online' => $isOnline,
|
||||
'avg_time' => $avgTime,
|
||||
'output' => implode("\n", $output),
|
||||
'return_code' => $returnVar,
|
||||
'command' => $command,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find ping command path.
|
||||
*/
|
||||
protected function findPingCommand(bool $isWindows): ?string
|
||||
{
|
||||
if ($isWindows) {
|
||||
return 'ping';
|
||||
}
|
||||
|
||||
// Check common Linux locations
|
||||
$locations = ['/bin/ping', '/usr/bin/ping', '/sbin/ping', '/usr/sbin/ping'];
|
||||
|
||||
foreach ($locations as $location) {
|
||||
if (file_exists($location) && is_executable($location)) {
|
||||
return $location;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find using which
|
||||
exec('which ping 2>&1', $whichOutput, $whichReturn);
|
||||
if ($whichReturn === 0 && !empty($whichOutput[0])) {
|
||||
return trim($whichOutput[0]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* TCP connection check as fallback when ping is not available.
|
||||
*/
|
||||
protected function tcpCheck(string $host): array
|
||||
{
|
||||
$port = 80;
|
||||
$timeout = 5;
|
||||
$startTime = microtime(true);
|
||||
|
||||
$fp = @fsockopen($host, $port, $errno, $errstr, $timeout);
|
||||
|
||||
$endTime = microtime(true);
|
||||
$responseTime = (int) round(($endTime - $startTime) * 1000);
|
||||
|
||||
$isOnline = $fp !== false;
|
||||
|
||||
if ($fp) {
|
||||
fclose($fp);
|
||||
}
|
||||
|
||||
return [
|
||||
'is_online' => $isOnline,
|
||||
'avg_time' => $isOnline ? $responseTime : null,
|
||||
'output' => $isOnline ? "TCP connection successful on port {$port}" : "TCP connection failed: {$errstr}",
|
||||
'return_code' => $isOnline ? 0 : $errno,
|
||||
'command' => "fsockopen({$host}, {$port}, {$timeout}s)",
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse average response time from ping output.
|
||||
*/
|
||||
protected function parseAverageTime(array $output, bool $isWindows): ?int
|
||||
{
|
||||
$outputText = implode("\n", $output);
|
||||
|
||||
if ($isWindows) {
|
||||
// Windows format: "Average = 23ms"
|
||||
if (preg_match('/Average\s*=\s*(\d+)ms/i', $outputText, $matches)) {
|
||||
return (int) $matches[1];
|
||||
}
|
||||
} else {
|
||||
// Linux format: "rtt min/avg/max/mdev = 12.345/23.456/34.567/8.901 ms"
|
||||
if (preg_match('/rtt min\/avg\/max\/mdev = [\d.]+\/([\d.]+)\/[\d.]+\/[\d.]+ ms/i', $outputText, $matches)) {
|
||||
return (int) round((float) $matches[1]);
|
||||
}
|
||||
|
||||
// Alternative Linux format: "round-trip min/avg/max = 12.345/23.456/34.567 ms"
|
||||
if (preg_match('/round-trip min\/avg\/max = [\d.]+\/([\d.]+)\/[\d.]+ ms/i', $outputText, $matches)) {
|
||||
return (int) round((float) $matches[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a job failure.
|
||||
*/
|
||||
public function failed(\Throwable $exception): void
|
||||
{
|
||||
Log::error('CheckSitePing: Job failed after all retries', [
|
||||
'site_id' => $this->site->id,
|
||||
'exception' => $exception->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\TcpPort;
|
||||
use App\Notifications\TcpPortAlert;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CheckTcpPort implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* The number of times the job may be attempted.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $tries = 3;
|
||||
|
||||
/**
|
||||
* The number of seconds the job can run before timing out.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $timeout = 30;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public TcpPort $tcpPort
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
$host = $this->extractHost($this->tcpPort->site->url);
|
||||
|
||||
if (!$host) {
|
||||
Log::error('CheckTcpPort: Invalid URL', [
|
||||
'tcp_port_id' => $this->tcpPort->id,
|
||||
'site_id' => $this->tcpPort->site_id,
|
||||
'url' => $this->tcpPort->site->url,
|
||||
]);
|
||||
|
||||
$this->tcpPort->update([
|
||||
'is_open' => false,
|
||||
'response_time_ms' => null,
|
||||
'last_checked_at' => now(),
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->checkPort($host, $this->tcpPort->port);
|
||||
|
||||
$wasOpen = $this->tcpPort->is_open;
|
||||
|
||||
$this->tcpPort->update([
|
||||
'is_open' => $result['is_open'],
|
||||
'response_time_ms' => $result['response_time'],
|
||||
'last_checked_at' => now(),
|
||||
]);
|
||||
|
||||
Log::info('CheckTcpPort: Check completed', [
|
||||
'tcp_port_id' => $this->tcpPort->id,
|
||||
'site_id' => $this->tcpPort->site_id,
|
||||
'host' => $host,
|
||||
'port' => $this->tcpPort->port,
|
||||
'is_open' => $result['is_open'],
|
||||
'response_time_ms' => $result['response_time'],
|
||||
]);
|
||||
|
||||
// Send notification if port status changed from open to closed
|
||||
if ($wasOpen === true && $result['is_open'] === false) {
|
||||
$this->tcpPort->site->user->notify(new TcpPortAlert(
|
||||
$this->tcpPort,
|
||||
'closed'
|
||||
));
|
||||
|
||||
Log::warning('CheckTcpPort: Port closed alert sent', [
|
||||
'tcp_port_id' => $this->tcpPort->id,
|
||||
'port' => $this->tcpPort->port,
|
||||
]);
|
||||
}
|
||||
|
||||
// Optionally send notification if port opened
|
||||
if ($wasOpen === false && $result['is_open'] === true) {
|
||||
$this->tcpPort->site->user->notify(new TcpPortAlert(
|
||||
$this->tcpPort,
|
||||
'opened'
|
||||
));
|
||||
|
||||
Log::info('CheckTcpPort: Port opened alert sent', [
|
||||
'tcp_port_id' => $this->tcpPort->id,
|
||||
'port' => $this->tcpPort->port,
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('CheckTcpPort: Failed', [
|
||||
'tcp_port_id' => $this->tcpPort->id,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
$this->tcpPort->update([
|
||||
'is_open' => false,
|
||||
'response_time_ms' => null,
|
||||
'last_checked_at' => now(),
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract host from URL.
|
||||
*/
|
||||
protected function extractHost(string $url): ?string
|
||||
{
|
||||
$parsed = parse_url($url);
|
||||
return $parsed['host'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a TCP port is open.
|
||||
*/
|
||||
protected function checkPort(string $host, int $port): array
|
||||
{
|
||||
$timeout = 5;
|
||||
$startTime = microtime(true);
|
||||
|
||||
$errno = 0;
|
||||
$errstr = '';
|
||||
|
||||
// Suppress warnings and capture errors
|
||||
$fp = @fsockopen($host, $port, $errno, $errstr, $timeout);
|
||||
|
||||
$endTime = microtime(true);
|
||||
$responseTime = (int) round(($endTime - $startTime) * 1000);
|
||||
|
||||
$isOpen = $fp !== false;
|
||||
|
||||
if ($fp) {
|
||||
fclose($fp);
|
||||
}
|
||||
|
||||
if (!$isOpen) {
|
||||
Log::debug('CheckTcpPort: Port check failed', [
|
||||
'host' => $host,
|
||||
'port' => $port,
|
||||
'errno' => $errno,
|
||||
'errstr' => $errstr,
|
||||
'response_time_ms' => $responseTime,
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
'is_open' => $isOpen,
|
||||
'response_time' => $isOpen ? $responseTime : null,
|
||||
'error_code' => $errno,
|
||||
'error_message' => $errstr,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a job failure.
|
||||
*/
|
||||
public function failed(\Throwable $exception): void
|
||||
{
|
||||
Log::error('CheckTcpPort: Job failed after all retries', [
|
||||
'tcp_port_id' => $this->tcpPort->id,
|
||||
'exception' => $exception->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -87,8 +87,10 @@ class CheckUptime implements ShouldQueue
|
||||
// Store the uptime check result
|
||||
UptimeCheck::create([
|
||||
'site_id' => $this->site->id,
|
||||
'check_type' => 'http',
|
||||
'status_code' => $statusCode,
|
||||
'response_time_ms' => $responseTime,
|
||||
'is_online' => $statusCode !== null && $statusCode < 400,
|
||||
'checked_at' => now(),
|
||||
]);
|
||||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class DnsRecord extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'site_id',
|
||||
'domain',
|
||||
'record_type',
|
||||
'expected_value',
|
||||
'last_checked_at',
|
||||
'is_valid',
|
||||
'actual_value',
|
||||
'error_message',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'last_checked_at' => 'datetime',
|
||||
'is_valid' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
'actual_value' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* Available DNS record types.
|
||||
*/
|
||||
public static function getRecordTypes(): array
|
||||
{
|
||||
return [
|
||||
'A' => 'A (IPv4 Address)',
|
||||
'AAAA' => 'AAAA (IPv6 Address)',
|
||||
'CNAME' => 'CNAME (Canonical Name)',
|
||||
'MX' => 'MX (Mail Exchange)',
|
||||
'TXT' => 'TXT (Text Record)',
|
||||
'NS' => 'NS (Name Server)',
|
||||
'SOA' => 'SOA (Start of Authority)',
|
||||
'PTR' => 'PTR (Pointer)',
|
||||
'SRV' => 'SRV (Service)',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the site that owns the DNS record.
|
||||
*/
|
||||
public function site(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Site::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter active DNS records.
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter valid records.
|
||||
*/
|
||||
public function scopeValid($query)
|
||||
{
|
||||
return $query->where('is_valid', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter invalid records.
|
||||
*/
|
||||
public function scopeInvalid($query)
|
||||
{
|
||||
return $query->where('is_valid', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status badge color.
|
||||
*/
|
||||
public function getStatusColorAttribute(): string
|
||||
{
|
||||
if ($this->is_valid === null) {
|
||||
return 'secondary';
|
||||
}
|
||||
|
||||
return $this->is_valid ? 'success' : 'danger';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status text.
|
||||
*/
|
||||
public function getStatusTextAttribute(): string
|
||||
{
|
||||
if ($this->is_valid === null) {
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
return $this->is_valid ? 'Valid' : 'Invalid';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display name for the record.
|
||||
*/
|
||||
public function getDisplayNameAttribute(): string
|
||||
{
|
||||
return "{$this->record_type} - {$this->domain}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted actual value for display.
|
||||
*/
|
||||
public function getFormattedActualValueAttribute(): ?string
|
||||
{
|
||||
if (!$this->actual_value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_array($this->actual_value)) {
|
||||
if (count($this->actual_value) === 1) {
|
||||
return $this->actual_value[0];
|
||||
}
|
||||
return implode(', ', $this->actual_value);
|
||||
}
|
||||
|
||||
return $this->actual_value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class HealthCheck extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'site_id',
|
||||
'endpoint',
|
||||
'expected_json_keys',
|
||||
'last_checked_at',
|
||||
'is_healthy',
|
||||
'response_data',
|
||||
'response_time_ms',
|
||||
'performance_threshold',
|
||||
'status_code',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'expected_json_keys' => 'array',
|
||||
'response_data' => 'array',
|
||||
'last_checked_at' => 'datetime',
|
||||
'is_healthy' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the site that owns the health check.
|
||||
*/
|
||||
public function site(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Site::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter active health checks.
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter healthy checks.
|
||||
*/
|
||||
public function scopeHealthy($query)
|
||||
{
|
||||
return $query->where('is_healthy', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter unhealthy checks.
|
||||
*/
|
||||
public function scopeUnhealthy($query)
|
||||
{
|
||||
return $query->where('is_healthy', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full URL for the health check endpoint.
|
||||
*/
|
||||
public function getFullUrlAttribute(): string
|
||||
{
|
||||
$siteUrl = rtrim($this->site->url, '/');
|
||||
$endpoint = ltrim($this->endpoint, '/');
|
||||
|
||||
return $siteUrl . '/' . $endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status badge color.
|
||||
*/
|
||||
public function getStatusColorAttribute(): string
|
||||
{
|
||||
if ($this->is_healthy === null) {
|
||||
return 'secondary';
|
||||
}
|
||||
|
||||
return $this->is_healthy ? 'success' : 'danger';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status text.
|
||||
*/
|
||||
public function getStatusTextAttribute(): string
|
||||
{
|
||||
if ($this->is_healthy === null) {
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
return $this->is_healthy ? 'Healthy' : 'Unhealthy';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display name for the endpoint.
|
||||
*/
|
||||
public function getDisplayNameAttribute(): string
|
||||
{
|
||||
return $this->endpoint ?: 'Health Check';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if response contains all expected keys.
|
||||
*/
|
||||
public function validateResponseKeys(array $responseData): bool
|
||||
{
|
||||
if (empty($this->expected_json_keys)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($this->expected_json_keys as $key) {
|
||||
if (!$this->hasNestedKey($responseData, $key)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if nested key exists in array using dot notation.
|
||||
*/
|
||||
protected function hasNestedKey(array $array, string $key): bool
|
||||
{
|
||||
if (array_key_exists($key, $array)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (explode('.', $key) as $segment) {
|
||||
if (!is_array($array) || !array_key_exists($segment, $array)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$array = $array[$segment];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -38,4 +38,19 @@ class Site extends Model
|
||||
{
|
||||
return $this->hasMany(BrokenLink::class);
|
||||
}
|
||||
|
||||
public function tcpPorts(): HasMany
|
||||
{
|
||||
return $this->hasMany(TcpPort::class);
|
||||
}
|
||||
|
||||
public function healthChecks(): HasMany
|
||||
{
|
||||
return $this->hasMany(HealthCheck::class);
|
||||
}
|
||||
|
||||
public function dnsRecords(): HasMany
|
||||
{
|
||||
return $this->hasMany(DnsRecord::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class TcpPort extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'site_id',
|
||||
'port',
|
||||
'name',
|
||||
'is_active',
|
||||
'last_checked_at',
|
||||
'is_open',
|
||||
'response_time_ms',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'is_open' => 'boolean',
|
||||
'last_checked_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the site that owns the TCP port.
|
||||
*/
|
||||
public function site(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Site::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter active ports.
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter open ports.
|
||||
*/
|
||||
public function scopeOpen($query)
|
||||
{
|
||||
return $query->where('is_open', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter closed ports.
|
||||
*/
|
||||
public function scopeClosed($query)
|
||||
{
|
||||
return $query->where('is_open', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get common port names.
|
||||
*/
|
||||
public static function getCommonPorts(): array
|
||||
{
|
||||
return [
|
||||
21 => 'FTP',
|
||||
22 => 'SSH',
|
||||
23 => 'Telnet',
|
||||
25 => 'SMTP',
|
||||
53 => 'DNS',
|
||||
80 => 'HTTP',
|
||||
110 => 'POP3',
|
||||
143 => 'IMAP',
|
||||
443 => 'HTTPS',
|
||||
465 => 'SMTPS',
|
||||
587 => 'SMTP (Submission)',
|
||||
993 => 'IMAPS',
|
||||
995 => 'POP3S',
|
||||
3306 => 'MySQL',
|
||||
3389 => 'RDP',
|
||||
5432 => 'PostgreSQL',
|
||||
6379 => 'Redis',
|
||||
8080 => 'HTTP Alternate',
|
||||
8443 => 'HTTPS Alternate',
|
||||
27017 => 'MongoDB',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display name for the port.
|
||||
*/
|
||||
public function getDisplayNameAttribute(): string
|
||||
{
|
||||
if ($this->name) {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
$commonPorts = self::getCommonPorts();
|
||||
return $commonPorts[$this->port] ?? "Port {$this->port}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status badge color.
|
||||
*/
|
||||
public function getStatusColorAttribute(): string
|
||||
{
|
||||
if ($this->is_open === null) {
|
||||
return 'secondary';
|
||||
}
|
||||
|
||||
return $this->is_open ? 'success' : 'danger';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status text.
|
||||
*/
|
||||
public function getStatusTextAttribute(): string
|
||||
{
|
||||
if ($this->is_open === null) {
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
return $this->is_open ? 'Open' : 'Closed';
|
||||
}
|
||||
}
|
||||
@@ -11,15 +11,42 @@ class UptimeCheck extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'site_id',
|
||||
'check_type',
|
||||
'status_code',
|
||||
'response_time_ms',
|
||||
'is_online',
|
||||
'checked_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'checked_at' => 'datetime',
|
||||
'is_online' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* Scope to filter by check type.
|
||||
*/
|
||||
public function scopeOfType($query, string $type)
|
||||
{
|
||||
return $query->where('check_type', $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter HTTP checks.
|
||||
*/
|
||||
public function scopeHttpChecks($query)
|
||||
{
|
||||
return $query->where('check_type', 'http');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter ping checks.
|
||||
*/
|
||||
public function scopePingChecks($query)
|
||||
{
|
||||
return $query->where('check_type', 'ping');
|
||||
}
|
||||
|
||||
public function site(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Site::class);
|
||||
|
||||
+2
-1
@@ -7,11 +7,12 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
use HasFactory, Notifiable, HasApiTokens;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\DnsRecord;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class DnsRecordAlert extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public DnsRecord $dnsRecord,
|
||||
public string $status,
|
||||
public ?array $actualValue = null,
|
||||
public ?string $errorMessage = null
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail', 'database'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*/
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$subject = $this->status === 'invalid'
|
||||
? "DNS Record Invalid: {$this->dnsRecord->domain}"
|
||||
: "DNS Record Restored: {$this->dnsRecord->domain}";
|
||||
|
||||
$message = (new MailMessage)
|
||||
->subject($subject);
|
||||
|
||||
if ($this->status === 'invalid') {
|
||||
$message->error()
|
||||
->line("DNS record validation failed for {$this->dnsRecord->record_type} record of {$this->dnsRecord->domain}")
|
||||
->line("This could indicate a DNS configuration issue or propagation delay.");
|
||||
|
||||
if ($this->errorMessage) {
|
||||
$message->line("Error: {$this->errorMessage}");
|
||||
} elseif ($this->actualValue && $this->dnsRecord->expected_value) {
|
||||
$actualFormatted = is_array($this->actualValue) ? implode(', ', $this->actualValue) : $this->actualValue;
|
||||
$message->line("Expected: {$this->dnsRecord->expected_value}")
|
||||
->line("Actual: {$actualFormatted}");
|
||||
}
|
||||
} else {
|
||||
$message->success()
|
||||
->line("DNS record for {$this->dnsRecord->record_type} of {$this->dnsRecord->domain} is now valid.")
|
||||
->line("The DNS configuration has been restored.");
|
||||
|
||||
if ($this->actualValue) {
|
||||
$actualFormatted = is_array($this->actualValue) ? implode(', ', $this->actualValue) : $this->actualValue;
|
||||
$message->line("Current value: {$actualFormatted}");
|
||||
}
|
||||
}
|
||||
|
||||
return $message
|
||||
->line("Domain: {$this->dnsRecord->domain}")
|
||||
->line("Record Type: {$this->dnsRecord->record_type}")
|
||||
->line("Checked at: {$this->dnsRecord->last_checked_at->format('Y-m-d H:i:s')}")
|
||||
->action('View Details', url("/sites/{$this->dnsRecord->site_id}/dns-records/{$this->dnsRecord->id}"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'type' => 'dns_record',
|
||||
'dns_record_id' => $this->dnsRecord->id,
|
||||
'site_id' => $this->dnsRecord->site_id,
|
||||
'site_name' => $this->dnsRecord->site->name,
|
||||
'domain' => $this->dnsRecord->domain,
|
||||
'record_type' => $this->dnsRecord->record_type,
|
||||
'status' => $this->status,
|
||||
'is_valid' => $this->dnsRecord->is_valid,
|
||||
'expected_value' => $this->dnsRecord->expected_value,
|
||||
'actual_value' => $this->actualValue,
|
||||
'error_message' => $this->errorMessage,
|
||||
'checked_at' => $this->dnsRecord->last_checked_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\HealthCheck;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class HealthCheckAlert extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public HealthCheck $healthCheck,
|
||||
public string $status,
|
||||
public ?int $statusCode = null,
|
||||
public ?int $responseTime = null
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail', 'database'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*/
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$subject = $this->status === 'unhealthy'
|
||||
? "Health Check Failed: {$this->healthCheck->endpoint}"
|
||||
: "Health Check Restored: {$this->healthCheck->endpoint}";
|
||||
|
||||
$message = (new MailMessage)
|
||||
->subject($subject)
|
||||
->line("Health check alert for {$this->healthCheck->site->name}");
|
||||
|
||||
if ($this->status === 'unhealthy') {
|
||||
$message->error()
|
||||
->line("The health endpoint {$this->healthCheck->endpoint} is reporting as unhealthy.")
|
||||
->line("This could indicate an application issue or service degradation.");
|
||||
|
||||
if ($this->statusCode) {
|
||||
$message->line("HTTP Status Code: {$this->statusCode}");
|
||||
}
|
||||
} else {
|
||||
$message->success()
|
||||
->line("The health endpoint {$this->healthCheck->endpoint} is now healthy.")
|
||||
->line("The service has been restored.");
|
||||
|
||||
if ($this->statusCode) {
|
||||
$message->line("HTTP Status Code: {$this->statusCode}");
|
||||
}
|
||||
|
||||
if ($this->responseTime) {
|
||||
$message->line("Response Time: {$this->responseTime}ms");
|
||||
}
|
||||
}
|
||||
|
||||
return $message
|
||||
->line("Endpoint: {$this->healthCheck->full_url}")
|
||||
->line("Checked at: {$this->healthCheck->last_checked_at->format('Y-m-d H:i:s')}")
|
||||
->action('View Details', url("/sites/{$this->healthCheck->site_id}"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'type' => 'health_check',
|
||||
'health_check_id' => $this->healthCheck->id,
|
||||
'site_id' => $this->healthCheck->site_id,
|
||||
'site_name' => $this->healthCheck->site->name,
|
||||
'endpoint' => $this->healthCheck->endpoint,
|
||||
'full_url' => $this->healthCheck->full_url,
|
||||
'status' => $this->status,
|
||||
'is_healthy' => $this->healthCheck->is_healthy,
|
||||
'status_code' => $this->statusCode,
|
||||
'response_time_ms' => $this->responseTime,
|
||||
'checked_at' => $this->healthCheck->last_checked_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\HealthCheck;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class PerformanceAlert extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public string $resourceType,
|
||||
public string $resourceName,
|
||||
public int $responseTime,
|
||||
public int $threshold,
|
||||
public ?string $url = null,
|
||||
public ?int $resourceId = null
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail', 'database'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*/
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$subject = "Performance Alert: {$this->resourceName}";
|
||||
$percentageOver = round((($this->responseTime - $this->threshold) / $this->threshold) * 100);
|
||||
|
||||
$message = (new MailMessage)
|
||||
->subject($subject)
|
||||
->warning()
|
||||
->line("Performance threshold exceeded for {$this->resourceType}: {$this->resourceName}")
|
||||
->line("Response time ({$this->responseTime}ms) exceeded the threshold of {$this->threshold}ms by {$percentageOver}%.")
|
||||
->line("This could indicate a performance degradation or resource constraint.");
|
||||
|
||||
$message->line("Response Time: {$this->responseTime}ms")
|
||||
->line("Threshold: {$this->threshold}ms")
|
||||
->line("Difference: " . ($this->responseTime - $this->threshold) . "ms");
|
||||
|
||||
if ($this->url) {
|
||||
$message->line("URL: {$this->url}");
|
||||
}
|
||||
|
||||
if ($this->resourceId) {
|
||||
$actionUrl = match($this->resourceType) {
|
||||
'Health Check' => url("/sites/{$this->resourceId}"),
|
||||
default => url('/dashboard'),
|
||||
};
|
||||
$message->action('View Details', $actionUrl);
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'type' => 'performance_alert',
|
||||
'resource_type' => $this->resourceType,
|
||||
'resource_name' => $this->resourceName,
|
||||
'resource_id' => $this->resourceId,
|
||||
'response_time_ms' => $this->responseTime,
|
||||
'threshold_ms' => $this->threshold,
|
||||
'difference_ms' => $this->responseTime - $this->threshold,
|
||||
'percentage_over' => round((($this->responseTime - $this->threshold) / $this->threshold) * 100, 2),
|
||||
'url' => $this->url,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\TcpPort;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class TcpPortAlert extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public TcpPort $tcpPort,
|
||||
public string $status
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail', 'database'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*/
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$subject = $this->status === 'closed'
|
||||
? "TCP Port {$this->tcpPort->port} is Closed"
|
||||
: "TCP Port {$this->tcpPort->port} is Now Open";
|
||||
|
||||
$message = (new MailMessage)
|
||||
->subject($subject)
|
||||
->line("Port monitoring alert for {$this->tcpPort->site->name}");
|
||||
|
||||
if ($this->status === 'closed') {
|
||||
$message->error()
|
||||
->line("Port {$this->tcpPort->display_name} ({$this->tcpPort->port}) is currently closed.")
|
||||
->line("This could indicate a service outage or firewall issue.");
|
||||
} else {
|
||||
$message->success()
|
||||
->line("Port {$this->tcpPort->display_name} ({$this->tcpPort->port}) is now accessible.")
|
||||
->line("The service has been restored.");
|
||||
}
|
||||
|
||||
return $message
|
||||
->line("Site: {$this->tcpPort->site->url}")
|
||||
->line("Checked at: {$this->tcpPort->last_checked_at->format('Y-m-d H:i:s')}")
|
||||
->action('View Details', url("/sites/{$this->tcpPort->site_id}"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'type' => 'tcp_port',
|
||||
'tcp_port_id' => $this->tcpPort->id,
|
||||
'site_id' => $this->tcpPort->site_id,
|
||||
'site_name' => $this->tcpPort->site->name,
|
||||
'port' => $this->tcpPort->port,
|
||||
'port_name' => $this->tcpPort->display_name,
|
||||
'status' => $this->status,
|
||||
'is_open' => $this->tcpPort->is_open,
|
||||
'checked_at' => $this->tcpPort->last_checked_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user