diff --git a/config/health.php b/config/health.php new file mode 100644 index 0000000..83deb2e --- /dev/null +++ b/config/health.php @@ -0,0 +1,167 @@ + [ + Spatie\Health\ResultStores\EloquentHealthResultStore::class => [ + 'connection' => env('HEALTH_DB_CONNECTION', env('DB_CONNECTION')), + 'model' => Spatie\Health\Models\HealthCheckResultHistoryItem::class, + 'keep_history_for_days' => 5, + ], + + /* + Spatie\Health\ResultStores\CacheHealthResultStore::class => [ + 'store' => 'file', + ], + + Spatie\Health\ResultStores\JsonFileHealthResultStore::class => [ + 'disk' => 's3', + 'path' => 'health.json', + ], + + Spatie\Health\ResultStores\InMemoryHealthResultStore::class, + */ + ], + + /* + * You can get notified when specific events occur. Out of the box you can use 'mail' and 'slack'. + * For Slack you need to install laravel/slack-notification-channel. + */ + 'notifications' => [ + /* + * Notifications will only get sent if this option is set to `true`. + */ + 'enabled' => true, + + 'notifications' => [ + Spatie\Health\Notifications\CheckFailedNotification::class => ['mail'], + ], + + /* + * Here you can specify the notifiable to which the notifications should be sent. The default + * notifiable will use the variables specified in this config file. + */ + 'notifiable' => Spatie\Health\Notifications\Notifiable::class, + + /* + * When checks start failing, you could potentially end up getting + * a notification every minute. + * + * With this setting, notifications are throttled. By default, you'll + * only get one notification per hour. + */ + 'throttle_notifications_for_minutes' => 60, + 'throttle_notifications_key' => 'health:latestNotificationSentAt:', + + 'mail' => [ + 'to' => 'your@example.com', + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', 'Example'), + ], + ], + + 'slack' => [ + 'webhook_url' => env('HEALTH_SLACK_WEBHOOK_URL', ''), + + /* + * If this is set to null the default channel of the webhook will be used. + */ + 'channel' => null, + + 'username' => null, + + 'icon' => null, + ], + ], + + /* + * You can let Oh Dear monitor the results of all health checks. This way, you'll + * get notified of any problems even if your application goes totally down. Via + * Oh Dear, you can also have access to more advanced notification options. + */ + 'oh_dear_endpoint' => [ + 'enabled' => false, + + /* + * When this option is enabled, the checks will run before sending a response. + * Otherwise, we'll send the results from the last time the checks have run. + */ + 'always_send_fresh_results' => true, + + /* + * The secret that is displayed at the Application Health settings at Oh Dear. + */ + 'secret' => env('OH_DEAR_HEALTH_CHECK_SECRET'), + + /* + * The URL that should be configured in the Application health settings at Oh Dear. + */ + 'url' => '/oh-dear-health-check-results', + ], + + /* + * You can specify a heartbeat URL for the Horizon check. + * This URL will be pinged if the Horizon check is successful. + * This way you can get notified if Horizon goes down. + */ + 'horizon' => [ + 'heartbeat_url' => env('HORIZON_HEARTBEAT_URL', null), + ], + + /* + * You can specify a heartbeat URL for the Schedule check. + * This URL will be pinged if the Schedule check is successful. + * This way you can get notified if the schedule fails to run. + */ + 'schedule' => [ + 'heartbeat_url' => env('SCHEDULE_HEARTBEAT_URL', null), + ], + + /* + * You can specify a heartbeat URL for the Reverb check. + * This URL will be pinged if the Reverb check is successful. + * This way you can get notified if Reverb goes down. + */ + 'reverb' => [ + 'heartbeat_url' => env('REVERB_HEARTBEAT_URL', null), + ], + + /* + * You can set a theme for the local results page + * + * - light: light mode + * - dark: dark mode + */ + 'theme' => 'light', + + /* + * When enabled, completed `HealthQueueJob`s will be displayed + * in Horizon's silenced jobs screen. + */ + 'silence_health_queue_job' => true, + + /* + * The response code to use for HealthCheckJsonResultsController when a health + * check has failed + */ + 'json_results_failure_status' => 200, + + /* + * You can specify a secret token that needs to be sent in the X-Secret-Token for secured access. + */ + 'secret_token' => env('HEALTH_SECRET_TOKEN') ?? null, + +/** + * By default, conditionally skipped health checks are treated as failures. + * You can override this behavior by uncommenting the configuration below. + * + * @link https://spatie.be/docs/laravel-health/v1/basic-usage/conditionally-running-or-modifying-checks + */ + // 'treat_skipped_as_failure' => false +]; diff --git a/src/Console/Commands/SetupReverb.php b/src/Console/Commands/SetupReverb.php new file mode 100644 index 0000000..f145f39 --- /dev/null +++ b/src/Console/Commands/SetupReverb.php @@ -0,0 +1,125 @@ +environmentFile())) { + $this->components->error('Environment file not found.'); + + return self::FAILURE; + } + + $contents = File::get($env); + + // Check if credentials already exist + $hasCredentials = Str::contains($contents, 'REVERB_APP_ID=') && + ! Str::contains($contents, 'REVERB_APP_ID=null'); + + if ($hasCredentials && ! $this->option('force')) { + $this->components->warn('Reverb credentials already exist in .env file.'); + $this->components->info('Use --force to overwrite existing credentials.'); + + return self::SUCCESS; + } + + // Generate credentials using the same logic as Laravel Reverb + $appId = random_int(100_000, 999_999); + $appKey = Str::lower(Str::random(20)); + $appSecret = Str::lower(Str::random(20)); + + $this->updateEnvironmentFile($env, $contents, $appId, $appKey, $appSecret); + + $this->components->info('Reverb credentials generated successfully:'); + $this->components->twoColumnDetail('REVERB_APP_ID', $appId); + $this->components->twoColumnDetail('REVERB_APP_KEY', $appKey); + $this->components->twoColumnDetail('REVERB_APP_SECRET', '***'.substr($appSecret, -4)); + + return self::SUCCESS; + } + + /** + * Update the environment file with new Reverb credentials. + * + * This method handles both updating existing credentials and adding new ones. + * It uses regex replacement for existing values and appends new lines for missing keys. + * + * @param string $envPath Path to the .env file + * @param string $contents Current contents of the .env file + * @param int $appId Generated Reverb application ID + * @param string $appKey Generated Reverb application key + * @param string $appSecret Generated Reverb application secret + */ + protected function updateEnvironmentFile(string $envPath, string $contents, int $appId, string $appKey, string $appSecret): void + { + $replacements = [ + 'REVERB_APP_ID' => "REVERB_APP_ID={$appId}", + 'REVERB_APP_KEY' => "REVERB_APP_KEY={$appKey}", + 'REVERB_APP_SECRET' => "REVERB_APP_SECRET={$appSecret}", + ]; + + $newContents = $contents; + + foreach ($replacements as $key => $value) { + if (Str::contains($newContents, "{$key}=")) { + // Replace existing value + $newContents = preg_replace( + "/^{$key}=.*$/m", + $value, + $newContents + ); + } else { + // Add new line if it doesn't exist + $newContents = rtrim($newContents).PHP_EOL.$value.PHP_EOL; + } + } + + File::put($envPath, $newContents); + } +} diff --git a/src/EclipseServiceProvider.php b/src/EclipseServiceProvider.php index ffeb8a8..9834568 100644 --- a/src/EclipseServiceProvider.php +++ b/src/EclipseServiceProvider.php @@ -8,6 +8,8 @@ use Eclipse\Core\Console\Commands\ClearCommand; use Eclipse\Core\Console\Commands\DeployCommand; use Eclipse\Core\Console\Commands\PostComposerUpdate; +use Eclipse\Core\Console\Commands\SetupReverb; +use Eclipse\Core\Health\Checks\ReverbCheck; use Eclipse\Core\Models\Locale; use Eclipse\Core\Models\User; use Eclipse\Core\Models\User\Permission; @@ -46,6 +48,7 @@ public function configurePackage(SpatiePackage|Package $package): void ->hasCommands([ ClearCommand::class, DeployCommand::class, + SetupReverb::class, PostComposerUpdate::class, ]) ->hasConfigFile([ @@ -58,6 +61,7 @@ public function configurePackage(SpatiePackage|Package $package): void 'settings', 'telescope', 'themes', + 'health', ]) ->hasViews() ->hasSettings() @@ -148,6 +152,7 @@ public function boot(): void ->failWhenUsedSpaceIsAbovePercentage(90), CacheCheck::new(), HorizonCheck::new(), + ReverbCheck::new(), RedisCheck::new(), ScheduleCheck::new(), SecurityAdvisoriesCheck::new(), diff --git a/src/Health/Checks/ReverbCheck.php b/src/Health/Checks/ReverbCheck.php new file mode 100644 index 0000000..71f7e46 --- /dev/null +++ b/src/Health/Checks/ReverbCheck.php @@ -0,0 +1,85 @@ +heartbeatUrl = $url; + + return $this; + } + + public function run(): Result + { + $result = Result::make(); + + // Check if Reverb is configured + if (! config('reverb.servers.reverb')) { + return $result->failed('Reverb does not seem to be configured correctly.'); + } + + $config = config('reverb.servers.reverb'); + $configHost = $config['host'] ?? '127.0.0.1'; + $port = $config['port'] ?? 8080; + + // If server is bound to 0.0.0.0, we should connect to localhost + $host = $configHost === '0.0.0.0' ? '127.0.0.1' : $configHost; + + // Try to connect to the Reverb server using socket connection + try { + $socket = @fsockopen($host, $port, $errno, $errstr, 5); + + if (! $socket) { + // Check if there's a recent restart signal (might indicate server is starting) + $lastRestart = Cache::get('laravel:reverb:restart'); + if ($lastRestart && now()->diffInMinutes($lastRestart) < 2) { + return $result + ->warning('Reverb server might be restarting.') + ->shortSummary('Restarting'); + } + + return $result + ->failed("Cannot connect to Reverb server: {$errstr} ({$errno})") + ->shortSummary('Not running'); + } + + fclose($socket); + } catch (Exception $e) { + // Check if there's a recent restart signal (might indicate server is starting) + $lastRestart = Cache::get('laravel:reverb:restart'); + if ($lastRestart && now()->diffInMinutes($lastRestart) < 2) { + return $result + ->warning('Reverb server might be restarting.') + ->shortSummary('Restarting'); + } + + return $result + ->failed("Reverb server is not running or not accessible: {$e->getMessage()}") + ->shortSummary('Not running'); + } + + $heartbeatUrl = $this->heartbeatUrl ?? config('health.reverb.heartbeat_url'); + + if ($heartbeatUrl) { + $this->pingUrl($heartbeatUrl); + } + + return $result->ok()->shortSummary('Running'); + } +}