feat: verbose logging, file persistence, auto websocket log channel

- WebSocketHandler: log connection rejections, unknown app keys, message drops
- Logger: persist all output to file via Laravel Log facade
- ServiceProvider: auto-register 'websocket' daily log channel
This commit is contained in:
Fabian @ Blax Software 2026-04-16 08:17:39 +02:00
parent 781e329601
commit 859fcb6f89
3 changed files with 69 additions and 2 deletions

View File

@ -2,6 +2,7 @@
namespace BlaxSoftware\LaravelWebSockets\Server\Loggers; namespace BlaxSoftware\LaravelWebSockets\Server\Loggers;
use Illuminate\Support\Facades\Log;
use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
@ -15,14 +16,14 @@ class Logger
protected $consoleOutput; protected $consoleOutput;
/** /**
* Wether the logger is enabled. * Whether the logger is enabled.
* *
* @var bool * @var bool
*/ */
protected $enabled = false; protected $enabled = false;
/** /**
* Wether the verbose mode is on. * Whether the verbose mode is on.
* *
* @var bool * @var bool
*/ */
@ -116,10 +117,38 @@ class Logger
$this->line($message, 'error'); $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) protected function line(string $message, string $style)
{ {
// Console output (existing behavior)
$this->consoleOutput->writeln( $this->consoleOutput->writeln(
$style ? "<{$style}>{$message}</{$style}>" : $message $style ? "<{$style}>{$message}</{$style}>" : $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
}
} }
} }

View File

@ -9,6 +9,7 @@ use BlaxSoftware\LaravelWebSockets\Events\NewConnection;
use BlaxSoftware\LaravelWebSockets\Helpers; use BlaxSoftware\LaravelWebSockets\Helpers;
use BlaxSoftware\LaravelWebSockets\Server\Exceptions\WebSocketException; use BlaxSoftware\LaravelWebSockets\Server\Exceptions\WebSocketException;
use Exception; use Exception;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use Ratchet\RFC6455\Messaging\MessageInterface; use Ratchet\RFC6455\Messaging\MessageInterface;
@ -45,6 +46,7 @@ class WebSocketHandler implements MessageComponentInterface
public function onOpen(ConnectionInterface $connection) public function onOpen(ConnectionInterface $connection)
{ {
if (! $this->connectionCanBeMade($connection)) { if (! $this->connectionCanBeMade($connection)) {
$this->wsLog('warning', 'Connection rejected: server not accepting new connections');
return $connection->close(); return $connection->close();
} }
@ -64,6 +66,8 @@ class WebSocketHandler implements MessageComponentInterface
$this->channelManager->connectionPonged($connection); $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); NewConnection::dispatch($connection->app->id, $connection->socketId);
} }
} catch (WebSocketException $exception) { } catch (WebSocketException $exception) {
@ -84,6 +88,7 @@ class WebSocketHandler implements MessageComponentInterface
public function onMessage(ConnectionInterface $connection, MessageInterface $message) public function onMessage(ConnectionInterface $connection, MessageInterface $message)
{ {
if (! isset($connection->app)) { if (! isset($connection->app)) {
$this->wsLog('warning', 'Message dropped: connection has no app (likely failed auth). Payload: '.Str::limit($message->getPayload(), 200));
return; return;
} }
@ -170,6 +175,10 @@ class WebSocketHandler implements MessageComponentInterface
$exception->getPayload() $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) App::findByKey($appKey)
->then(function ($app) use ($appKey, $connection, $deferred) { ->then(function ($app) use ($appKey, $connection, $deferred) {
if (! $app) { 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)); $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); 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
}
}
} }

View File

@ -25,6 +25,7 @@ class WebSocketsServiceProvider extends ServiceProvider
__DIR__ . '/Websocket' => app_path('Websocket') __DIR__ . '/Websocket' => app_path('Websocket')
]); ]);
$this->registerWebsocketLogChannel();
$this->registerDefaultWebsocketChannels(); $this->registerDefaultWebsocketChannels();
$this->registerEventLoop(); $this->registerEventLoop();
$this->registerWebSocketHandler(); $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() protected function registerWebSocketHandler()
{ {
$this->app->singleton('websockets.handler', function () { $this->app->singleton('websockets.handler', function () {