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;
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}</{$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\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
}
}
}

View File

@ -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 () {