F websockets:watch — live-updating stats display, refreshes every 1s
Renders the same Live Stats / channels table that `websockets:info` shows at the bottom, but loops indefinitely so it can be left open as a quick status pane. 1-second poll against the existing WebsocketService cache reads — no pub/sub plumbing because there is no "stats changed" event emitted today and a 1s tick is fast enough for the granularity humans care about. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
18c58b9be6
commit
e1abef4194
|
|
@ -0,0 +1,105 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace BlaxSoftware\LaravelWebSockets\Console\Commands;
|
||||||
|
|
||||||
|
use BlaxSoftware\LaravelWebSockets\Services\WebsocketService;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Live-updating display of WebSocket connection stats.
|
||||||
|
*
|
||||||
|
* Renders only the stats portion of `websockets:info` (total connections,
|
||||||
|
* authenticated users, active channels, per-channel breakdown) and refreshes
|
||||||
|
* every second by default.
|
||||||
|
*
|
||||||
|
* Polling vs. event-driven: stats live in the cache store (Redis in prod) and
|
||||||
|
* are written by the running websocket process. There is no pub/sub channel
|
||||||
|
* that emits a "stats changed" event, so a true event-driven approach would
|
||||||
|
* require either tailing the cache writes or adding pub/sub plumbing into
|
||||||
|
* WebsocketService. A 1-second poll is well under the granularity at which
|
||||||
|
* connection counts are interesting and avoids that infrastructure cost.
|
||||||
|
*/
|
||||||
|
class WatchStats extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'websockets:watch
|
||||||
|
{--interval=1 : Refresh interval in seconds (minimum 1)}';
|
||||||
|
|
||||||
|
protected $description = 'Live WebSocket stats display — refreshes every second until Ctrl+C.';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$interval = max(1, (int) $this->option('interval'));
|
||||||
|
|
||||||
|
// Restore cursor visibility on Ctrl+C / kill so the terminal isn't
|
||||||
|
// left in a broken state if the user interrupts mid-render.
|
||||||
|
if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) {
|
||||||
|
pcntl_async_signals(true);
|
||||||
|
$restore = function () {
|
||||||
|
$this->output->write("\033[?25h");
|
||||||
|
$this->newLine();
|
||||||
|
exit(0);
|
||||||
|
};
|
||||||
|
pcntl_signal(SIGINT, $restore);
|
||||||
|
pcntl_signal(SIGTERM, $restore);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->output->write("\033[?25l"); // hide cursor for cleaner refresh
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
$this->output->write("\033[2J\033[H"); // clear screen + home cursor
|
||||||
|
|
||||||
|
$this->line('<fg=cyan;options=bold>WebSocket Server — Live Stats</>');
|
||||||
|
$this->line('<fg=gray>' . now()->format('Y-m-d H:i:s') . ' — refreshing every ' . $interval . 's (Ctrl+C to exit)</>');
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
|
$this->renderStats();
|
||||||
|
|
||||||
|
sleep($interval);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
$this->output->write("\033[?25h"); // always restore cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0; // @phpstan-ignore-line
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identical to ServerInfo::renderStats — duplicated rather than shared via
|
||||||
|
* trait so that the two commands can evolve independently if needed (e.g.
|
||||||
|
* the live view may eventually want sparkline-style deltas that the
|
||||||
|
* one-shot view doesn't).
|
||||||
|
*/
|
||||||
|
protected function renderStats(): void
|
||||||
|
{
|
||||||
|
$channels = WebsocketService::getActiveChannels();
|
||||||
|
$authedUsers = WebsocketService::getAuthedUsers();
|
||||||
|
|
||||||
|
$totalConnections = 0;
|
||||||
|
$channelData = [];
|
||||||
|
|
||||||
|
foreach ($channels as $channel) {
|
||||||
|
$connections = WebsocketService::getChannelConnections($channel);
|
||||||
|
$count = count($connections);
|
||||||
|
$totalConnections += $count;
|
||||||
|
$channelData[] = [$channel, $count];
|
||||||
|
}
|
||||||
|
|
||||||
|
$uniqueUsers = count(array_unique(array_values($authedUsers)));
|
||||||
|
|
||||||
|
$this->components->twoColumnDetail('<fg=cyan;options=bold>Live Stats</>');
|
||||||
|
$this->components->twoColumnDetail('Total connections', "<fg=white;options=bold>{$totalConnections}</>");
|
||||||
|
$this->components->twoColumnDetail('Authenticated users', "<fg=white;options=bold>{$uniqueUsers}</>");
|
||||||
|
$this->components->twoColumnDetail('Active channels', '<fg=white;options=bold>' . count($channels) . '</>');
|
||||||
|
|
||||||
|
if (count($channelData) > 0) {
|
||||||
|
$this->newLine();
|
||||||
|
$this->table(['Channel', 'Connections'], $channelData);
|
||||||
|
} else {
|
||||||
|
$this->newLine();
|
||||||
|
$this->line(' <fg=gray>No active channels.</>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -61,6 +61,7 @@ class WebSocketsServiceProvider extends ServiceProvider
|
||||||
Console\Commands\RestartHard::class,
|
Console\Commands\RestartHard::class,
|
||||||
Console\Commands\SteerServer::class,
|
Console\Commands\SteerServer::class,
|
||||||
Console\Commands\ServerInfo::class,
|
Console\Commands\ServerInfo::class,
|
||||||
|
Console\Commands\WatchStats::class,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue