diff --git a/src/Server/Loggers/Logger.php b/src/Server/Loggers/Logger.php index ea668ff..48955c5 100644 --- a/src/Server/Loggers/Logger.php +++ b/src/Server/Loggers/Logger.php @@ -2,6 +2,7 @@ namespace BlaxSoftware\LaravelWebSockets\Server\Loggers; +use Illuminate\Support\Facades\Log; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Output\OutputInterface; @@ -15,14 +16,14 @@ class Logger protected $consoleOutput; /** - * Wether the logger is enabled. + * Whether the logger is enabled. * * @var bool */ protected $enabled = false; /** - * Wether the verbose mode is on. + * Whether the verbose mode is on. * * @var bool */ @@ -116,10 +117,38 @@ class Logger $this->line($message, 'error'); } + /** + * Write a message to the console and persist it to the websocket log file. + */ protected function line(string $message, string $style) { + // Console output (existing behavior) $this->consoleOutput->writeln( $style ? "<{$style}>{$message}" : $message ); + + // Also persist to log file so errors are visible outside the console + $this->fileLog($style, $message); + } + + /** + * Write a message to the websocket log channel. + * Uses the 'websocket' channel if available, falls back to the default. + */ + protected function fileLog(string $level, string $message): void + { + // Map console styles to log levels + $logLevel = match ($level) { + 'error' => 'error', + 'warning' => 'warning', + default => 'info', + }; + + try { + $channel = config('logging.channels.websocket') ? 'websocket' : null; + Log::channel($channel)->log($logLevel, '[WebSocket] '.$message); + } catch (\Throwable) { + // Logging must never crash the WS server + } } } diff --git a/src/Server/WebSocketHandler.php b/src/Server/WebSocketHandler.php index 7bc1449..740c25a 100644 --- a/src/Server/WebSocketHandler.php +++ b/src/Server/WebSocketHandler.php @@ -9,6 +9,7 @@ use BlaxSoftware\LaravelWebSockets\Events\NewConnection; use BlaxSoftware\LaravelWebSockets\Helpers; use BlaxSoftware\LaravelWebSockets\Server\Exceptions\WebSocketException; use Exception; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; use Ratchet\ConnectionInterface; use Ratchet\RFC6455\Messaging\MessageInterface; @@ -45,6 +46,7 @@ class WebSocketHandler implements MessageComponentInterface public function onOpen(ConnectionInterface $connection) { if (! $this->connectionCanBeMade($connection)) { + $this->wsLog('warning', 'Connection rejected: server not accepting new connections'); return $connection->close(); } @@ -64,6 +66,8 @@ class WebSocketHandler implements MessageComponentInterface $this->channelManager->connectionPonged($connection); + $this->wsLog('info', "[{$connection->app->id}][{$connection->socketId}] Connection established (key: {$connection->app->key})"); + NewConnection::dispatch($connection->app->id, $connection->socketId); } } catch (WebSocketException $exception) { @@ -84,6 +88,7 @@ class WebSocketHandler implements MessageComponentInterface public function onMessage(ConnectionInterface $connection, MessageInterface $message) { if (! isset($connection->app)) { + $this->wsLog('warning', 'Message dropped: connection has no app (likely failed auth). Payload: '.Str::limit($message->getPayload(), 200)); return; } @@ -170,6 +175,10 @@ class WebSocketHandler implements MessageComponentInterface $exception->getPayload() )); } + + $appId = $connection->app->id ?? 'unknown'; + $socketId = $connection->socketId ?? 'unknown'; + $this->wsLog('error', "[{$appId}][{$socketId}] {$exception->getMessage()}"); } /** @@ -201,6 +210,7 @@ class WebSocketHandler implements MessageComponentInterface App::findByKey($appKey) ->then(function ($app) use ($appKey, $connection, $deferred) { if (! $app) { + $this->wsLog('error', "Unknown app key: '{$appKey}'. Check that PUSHER_APP_KEY in .env matches the key used by the frontend. Configured apps: ".implode(', ', array_map(fn ($a) => $a['key'] ?? 'null', config('websockets.apps', [])))); $deferred->reject(new Exceptions\UnknownAppKey($appKey)); } @@ -306,4 +316,18 @@ class WebSocketHandler implements MessageComponentInterface { return str_ends_with($event, '.' . $action) || str_ends_with($event, ':' . $action); } + + /** + * Log a WebSocket server message. + * Uses the 'websocket' channel if configured, falls back to the default channel. + */ + protected function wsLog(string $level, string $message): void + { + try { + $channel = config('logging.channels.websocket') ? 'websocket' : config('logging.default'); + Log::channel($channel)->log($level, '[WebSocket] '.$message); + } catch (\Throwable) { + // Logging must never break the server + } + } } diff --git a/src/WebSocketsServiceProvider.php b/src/WebSocketsServiceProvider.php index 099f739..b08b17a 100644 --- a/src/WebSocketsServiceProvider.php +++ b/src/WebSocketsServiceProvider.php @@ -25,6 +25,7 @@ class WebSocketsServiceProvider extends ServiceProvider __DIR__ . '/Websocket' => app_path('Websocket') ]); + $this->registerWebsocketLogChannel(); $this->registerDefaultWebsocketChannels(); $this->registerEventLoop(); $this->registerWebSocketHandler(); @@ -85,6 +86,19 @@ class WebSocketsServiceProvider extends ServiceProvider } } + protected function registerWebsocketLogChannel() + { + // Register a dedicated 'websocket' log channel if the app hasn't defined one + if (! config('logging.channels.websocket')) { + config(['logging.channels.websocket' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/websocket.log'), + 'level' => 'debug', + 'days' => 7, + ]]); + } + } + protected function registerWebSocketHandler() { $this->app->singleton('websockets.handler', function () {