diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 179f0b3..ef9bea0 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -21,5 +21,6 @@ + diff --git a/src/Statistics/Logger/RedisStatisticsLogger.php b/src/Statistics/Logger/RedisStatisticsLogger.php new file mode 100644 index 0000000..a5067ca --- /dev/null +++ b/src/Statistics/Logger/RedisStatisticsLogger.php @@ -0,0 +1,195 @@ +channelManager = $channelManager; + $this->driver = $driver; + $this->redis = Cache::getRedis(); + } + + /** + * Handle the incoming websocket message. + * + * @param mixed $appId + * @return void + */ + public function webSocketMessage($appId) + { + $this->ensureAppIsSet($appId) + ->hincrby($this->getHash($appId), 'websocket_message_count', 1); + } + + /** + * Handle the incoming API message. + * + * @param mixed $appId + * @return void + */ + public function apiMessage($appId) + { + $this->ensureAppIsSet($appId) + ->hincrby($this->getHash($appId), 'api_message_count', 1); + } + + /** + * Handle the new conection. + * + * @param mixed $appId + * @return void + */ + public function connection($appId) + { + $currentConnectionCount = $this->ensureAppIsSet($appId) + ->hincrby($this->getHash($appId), 'current_connection_count', 1); + + $currentPeakConnectionCount = $this->redis->hget($this->getHash($appId), 'peak_connection_count'); + + $peakConnectionCount = is_null($currentPeakConnectionCount) + ? 1 + : max($currentPeakConnectionCount, $currentConnectionCount); + + + $this->redis->hset($this->getHash($appId), 'peak_connection_count', $peakConnectionCount); + } + + /** + * Handle disconnections. + * + * @param mixed $appId + * @return void + */ + public function disconnection($appId) + { + $currentConnectionCount = $this->ensureAppIsSet($appId) + ->hincrby($this->getHash($appId), 'current_connection_count', -1); + + $currentPeakConnectionCount = $this->redis->hget($this->getHash($appId), 'peak_connection_count'); + + $peakConnectionCount = is_null($currentPeakConnectionCount) + ? 0 + : max($currentPeakConnectionCount, $currentConnectionCount); + + + $this->redis->hset($this->getHash($appId), 'peak_connection_count', $peakConnectionCount); + } + + /** + * Save all the stored statistics. + * + * @return void + */ + public function save() + { + foreach ($this->redis->smembers('laravel-websockets:apps') as $appId) { + if (! $statistic = $this->redis->hgetall($this->getHash($appId))) { + continue; + } + + $this->driver::create([ + 'app_id' => $appId, + 'peak_connection_count' => $statistic['peak_connection_count'] ?? 0, + 'websocket_message_count' => $statistic['websocket_message_count'] ?? 0, + 'api_message_count' => $statistic['api_message_count'] ?? 0, + ]); + + $currentConnectionCount = $this->channelManager->getConnectionCount($appId); + + $currentConnectionCount === 0 + ? $this->resetAppTraces($appId) + : $this->resetStatistics($appId, $currentConnectionCount); + } + } + + /** + * Ensure the app id is stored in the Redis database. + * + * @param mixed $appId + * @return \Illuminate\Redis\RedisManager + */ + protected function ensureAppIsSet($appId) + { + $this->redis->sadd('laravel-websockets:apps', $appId); + + return $this->redis; + } + + /** + * Reset the statistics to a specific connection count. + * + * @param mixed $appId + * @param int $currentConnectionCount + * @return void + */ + public function resetStatistics($appId, int $currentConnectionCount) + { + $this->redis->hset($this->getHash($appId), 'current_connection_count', $currentConnectionCount); + $this->redis->hset($this->getHash($appId), 'peak_connection_count', $currentConnectionCount); + $this->redis->hset($this->getHash($appId), 'websocket_message_count', 0); + $this->redis->hset($this->getHash($appId), 'api_message_count', 0); + } + + /** + * Remove all app traces from the database if no connections have been set + * in the meanwhile since last save. + * + * @param mixed $appId + * @return void + */ + public function resetAppTraces($appId) + { + $this->redis->hdel($this->getHash($appId), 'current_connection_count'); + $this->redis->hdel($this->getHash($appId), 'peak_connection_count'); + $this->redis->hdel($this->getHash($appId), 'websocket_message_count'); + $this->redis->hdel($this->getHash($appId), 'api_message_count'); + + $this->redis->srem('laravel-websockets:apps', $appId); + } + + /** + * Get the Redis hash name for the app. + * + * @param mixed $appId + * @return string + */ + protected function getHash($appId): string + { + return "laravel-websockets:app:{$appId}"; + } +} diff --git a/tests/Statistics/Logger/StatisticsLoggerTest.php b/tests/Statistics/Logger/StatisticsLoggerTest.php index c7f2365..1ae28e7 100644 --- a/tests/Statistics/Logger/StatisticsLoggerTest.php +++ b/tests/Statistics/Logger/StatisticsLoggerTest.php @@ -4,6 +4,7 @@ namespace BeyondCode\LaravelWebSockets\Tests\Statistics\Controllers; use BeyondCode\LaravelWebSockets\Facades\StatisticsLogger; use BeyondCode\LaravelWebSockets\Statistics\Logger\MemoryStatisticsLogger; +use BeyondCode\LaravelWebSockets\Statistics\Logger\RedisStatisticsLogger; use BeyondCode\LaravelWebSockets\Statistics\Logger\NullStatisticsLogger; use BeyondCode\LaravelWebSockets\Statistics\Models\WebSocketsStatisticsEntry; use BeyondCode\LaravelWebSockets\Tests\TestCase; @@ -92,4 +93,60 @@ class StatisticsLoggerTest extends TestCase $this->assertCount(0, WebSocketsStatisticsEntry::all()); } + + /** @test */ + public function it_counts_connections_with_redis_logger_with_no_data() + { + $connection = $this->getConnectedWebSocketConnection(['channel-1']); + + $logger = new RedisStatisticsLogger( + $this->channelManager, + $this->statisticsDriver + ); + + $logger->resetAppTraces('1234'); + + $logger->webSocketMessage($connection->app->id); + $logger->apiMessage($connection->app->id); + $logger->connection($connection->app->id); + $logger->disconnection($connection->app->id); + + $logger->save(); + + $this->assertCount(1, WebSocketsStatisticsEntry::all()); + + $entry = WebSocketsStatisticsEntry::first(); + + $this->assertEquals(1, $entry->peak_connection_count); + $this->assertEquals(1, $entry->websocket_message_count); + $this->assertEquals(1, $entry->api_message_count); + } + + /** @test */ + public function it_counts_connections_with_redis_logger_with_existing_data() + { + $connection = $this->getConnectedWebSocketConnection(['channel-1']); + + $logger = new RedisStatisticsLogger( + $this->channelManager, + $this->statisticsDriver + ); + + $logger->resetStatistics('1234', 0); + + $logger->webSocketMessage($connection->app->id); + $logger->apiMessage($connection->app->id); + $logger->connection($connection->app->id); + $logger->disconnection($connection->app->id); + + $logger->save(); + + $this->assertCount(1, WebSocketsStatisticsEntry::all()); + + $entry = WebSocketsStatisticsEntry::first(); + + $this->assertEquals(1, $entry->peak_connection_count); + $this->assertEquals(1, $entry->websocket_message_count); + $this->assertEquals(1, $entry->api_message_count); + } }