Merge pull request #492 from beyondcode/feature/redis-statistics-driver
[2.x] Redis Statistics Driver
This commit is contained in:
commit
f3b0608595
|
|
@ -214,6 +214,7 @@ return [
|
||||||
|
|
||||||
'logger' => \BeyondCode\LaravelWebSockets\Statistics\Logger\MemoryStatisticsLogger::class,
|
'logger' => \BeyondCode\LaravelWebSockets\Statistics\Logger\MemoryStatisticsLogger::class,
|
||||||
// 'logger' => \BeyondCode\LaravelWebSockets\Statistics\Logger\NullStatisticsLogger::class,
|
// 'logger' => \BeyondCode\LaravelWebSockets\Statistics\Logger\NullStatisticsLogger::class,
|
||||||
|
// 'logger' => \BeyondCode\LaravelWebSockets\Statistics\Logger\RedisStatisticsLogger::class,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -32,3 +32,23 @@ Now, when your app broadcasts the message, it will make sure the connection reac
|
||||||
The available drivers for replication are:
|
The available drivers for replication are:
|
||||||
|
|
||||||
- [Redis](redis)
|
- [Redis](redis)
|
||||||
|
|
||||||
|
## Configure the Statistics driver
|
||||||
|
|
||||||
|
If you work with multi-node environments, beside replication, you shall take a look at the statistics logger. Each time your user connects, disconnects or send a message, you can track the statistics. However, these are centralized in one place before they are dumped in the database.
|
||||||
|
|
||||||
|
Unfortunately, you might end up with multiple rows when multiple servers run in parallel.
|
||||||
|
|
||||||
|
To fix this, just change the `statistics.logger` class with a logger that is able to centralize the statistics in one place. For example, you might want to store them into a Redis instance:
|
||||||
|
|
||||||
|
```php
|
||||||
|
'statistics' => [
|
||||||
|
|
||||||
|
'logger' => \BeyondCode\LaravelWebSockets\Statistics\Logger\RedisStatisticsLogger::class,
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
Check the `websockets.php` config file for more details.
|
||||||
|
|
|
||||||
|
|
@ -34,4 +34,3 @@ You can set the connection name to the Redis database under `redis`:
|
||||||
```
|
```
|
||||||
|
|
||||||
The connections can be found in your `config/database.php` file, under the `redis` key. It defaults to connection `default`.
|
The connections can be found in your `config/database.php` file, under the `redis` key. It defaults to connection `default`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,206 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Statistics\Logger;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Apps\App;
|
||||||
|
use BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver;
|
||||||
|
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
|
||||||
|
use Illuminate\Cache\RedisLock;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
class RedisStatisticsLogger implements StatisticsLogger
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The Channel manager.
|
||||||
|
*
|
||||||
|
* @var \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager
|
||||||
|
*/
|
||||||
|
protected $channelManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The statistics driver instance.
|
||||||
|
*
|
||||||
|
* @var \BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver
|
||||||
|
*/
|
||||||
|
protected $driver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Redis manager instance.
|
||||||
|
*
|
||||||
|
* @var \Illuminate\Redis\RedisManager
|
||||||
|
*/
|
||||||
|
protected $redis;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the logger.
|
||||||
|
*
|
||||||
|
* @param \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager $channelManager
|
||||||
|
* @param \BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver $driver
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(ChannelManager $channelManager, StatisticsDriver $driver)
|
||||||
|
{
|
||||||
|
$this->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()
|
||||||
|
{
|
||||||
|
$this->lock()->get(function () {
|
||||||
|
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}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a new RedisLock instance to avoid race conditions.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Cache\CacheLock
|
||||||
|
*/
|
||||||
|
protected function lock()
|
||||||
|
{
|
||||||
|
return new RedisLock($this->redis, 'laravel-websockets:lock', 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Tests\Statistics\Logger;
|
namespace BeyondCode\LaravelWebSockets\Tests\Mocks;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Statistics\Logger\MemoryStatisticsLogger;
|
use BeyondCode\LaravelWebSockets\Statistics\Logger\MemoryStatisticsLogger;
|
||||||
|
|
||||||
class FakeStatisticsLogger extends MemoryStatisticsLogger
|
class FakeMemoryStatisticsLogger extends MemoryStatisticsLogger
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
|
|
@ -5,6 +5,7 @@ namespace BeyondCode\LaravelWebSockets\Tests\Statistics\Controllers;
|
||||||
use BeyondCode\LaravelWebSockets\Facades\StatisticsLogger;
|
use BeyondCode\LaravelWebSockets\Facades\StatisticsLogger;
|
||||||
use BeyondCode\LaravelWebSockets\Statistics\Logger\MemoryStatisticsLogger;
|
use BeyondCode\LaravelWebSockets\Statistics\Logger\MemoryStatisticsLogger;
|
||||||
use BeyondCode\LaravelWebSockets\Statistics\Logger\NullStatisticsLogger;
|
use BeyondCode\LaravelWebSockets\Statistics\Logger\NullStatisticsLogger;
|
||||||
|
use BeyondCode\LaravelWebSockets\Statistics\Logger\RedisStatisticsLogger;
|
||||||
use BeyondCode\LaravelWebSockets\Statistics\Models\WebSocketsStatisticsEntry;
|
use BeyondCode\LaravelWebSockets\Statistics\Models\WebSocketsStatisticsEntry;
|
||||||
use BeyondCode\LaravelWebSockets\Tests\TestCase;
|
use BeyondCode\LaravelWebSockets\Tests\TestCase;
|
||||||
|
|
||||||
|
|
@ -92,4 +93,68 @@ class StatisticsLoggerTest extends TestCase
|
||||||
|
|
||||||
$this->assertCount(0, WebSocketsStatisticsEntry::all());
|
$this->assertCount(0, WebSocketsStatisticsEntry::all());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_counts_connections_with_redis_logger_with_no_data()
|
||||||
|
{
|
||||||
|
$this->runOnlyOnRedisReplication();
|
||||||
|
|
||||||
|
config(['cache.default' => 'redis']);
|
||||||
|
|
||||||
|
$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()
|
||||||
|
{
|
||||||
|
$this->runOnlyOnRedisReplication();
|
||||||
|
|
||||||
|
config(['cache.default' => 'redis']);
|
||||||
|
|
||||||
|
$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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ use BeyondCode\LaravelWebSockets\PubSub\Drivers\RedisClient;
|
||||||
use BeyondCode\LaravelWebSockets\PubSub\ReplicationInterface;
|
use BeyondCode\LaravelWebSockets\PubSub\ReplicationInterface;
|
||||||
use BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver;
|
use BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver;
|
||||||
use BeyondCode\LaravelWebSockets\Tests\Mocks\Connection;
|
use BeyondCode\LaravelWebSockets\Tests\Mocks\Connection;
|
||||||
|
use BeyondCode\LaravelWebSockets\Tests\Mocks\FakeMemoryStatisticsLogger;
|
||||||
use BeyondCode\LaravelWebSockets\Tests\Mocks\Message;
|
use BeyondCode\LaravelWebSockets\Tests\Mocks\Message;
|
||||||
use BeyondCode\LaravelWebSockets\Tests\Statistics\Logger\FakeStatisticsLogger;
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
|
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
|
||||||
use GuzzleHttp\Psr7\Request;
|
use GuzzleHttp\Psr7\Request;
|
||||||
use Orchestra\Testbench\BrowserKit\TestCase as BaseTestCase;
|
use Orchestra\Testbench\BrowserKit\TestCase as BaseTestCase;
|
||||||
|
|
@ -58,7 +58,7 @@ abstract class TestCase extends BaseTestCase
|
||||||
|
|
||||||
$this->statisticsDriver = $this->app->make(StatisticsDriver::class);
|
$this->statisticsDriver = $this->app->make(StatisticsDriver::class);
|
||||||
|
|
||||||
StatisticsLogger::swap(new FakeStatisticsLogger(
|
StatisticsLogger::swap(new FakeMemoryStatisticsLogger(
|
||||||
$this->channelManager,
|
$this->channelManager,
|
||||||
app(StatisticsDriver::class)
|
app(StatisticsDriver::class)
|
||||||
));
|
));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue