*/ protected array $metrics = []; /** * Create an instance of the metrics handler. */ public function __construct( protected ServerProviderManager $serverProviderManager, protected ChannelManager $channels, protected PubSubProvider $pubSubProvider ) { // } /** * Gather the metrics for the given type. */ public function gather(Application $application, string $type, array $options = []): PromiseInterface { $metric = new PendingMetric( Str::random(10), $application, MetricType::from($type), $options ); return $this->serverProviderManager->subscribesToEvents() ? $this->gatherMetricsFromSubscribers($metric) : $this->promise($this->get($metric)); } /** * Get the metrics for the given type. */ public function get(PendingMetric $metric): array { return match ($metric->type()) { MetricType::CHANNEL => $this->channel($metric), MetricType::CHANNELS => $this->channels($metric), MetricType::CHANNEL_USERS => $this->channelUsers($metric), MetricType::CONNECTIONS => $this->connections($metric), default => [], }; } /** * Get the channel for the given application. */ protected function channel(PendingMetric $metric): array { return $this->info($metric->application(), $metric->option('channel'), $metric->option('info', '')); } /** * Get the channels for the given application. */ protected function channels(PendingMetric $metric): array { if ($metric->option('channels')) { return $this->infoForChannels($metric->application(), $metric->option('channels'), $metric->option('info', '')); } $channels = collect($this->channels->for($metric->application())->all()); if ($filter = ($metric->option('filter', false))) { $channels = $channels->filter(fn ($channel) => Str::startsWith($channel->name(), $filter)); } $channels = $channels->filter(fn ($channel) => count($channel->connections()) > 0); return $this->infoForChannels( $metric->application(), $channels->all(), $metric->option('info', '') ); } /** * Get the channel users for the given application. */ protected function channelUsers(PendingMetric $metric): array { $channel = $this->channels->for($metric->application())->find($metric->option('channel')); if (! $channel) { return []; } return collect($channel->connections()) ->map(fn ($connection) => $connection->data()) ->unique('user_id') ->map(fn ($data) => ['id' => $data['user_id']]) ->values() ->all(); } /** * Get the connections for the given application. */ protected function connections(PendingMetric $metric): array { return $this->channels->for($metric->application())->connections(); } /** * Gather metrics from all subscribers for the given type. */ protected function gatherMetricsFromSubscribers(PendingMetric $metric): PromiseInterface { $this->metrics[$metric->key()] = $metric; $deferred = $this->listenForMetrics($metric); $this->requestMetricsFromSubscribers($metric); return timeout($deferred->promise(), 10)->then( fn ($metrics) => $metrics, fn () => $this->metrics[$metric->key()]?->resolve() ?? [], )->then( fn ($metrics) => $this->mergeSubscriberMetrics($metrics, $metric->type()) )->finally( fn () => $this->stopListening($metric) ); } /** * Request metrics from all subscribers. */ protected function requestMetricsFromSubscribers(PendingMetric $metric): void { $this->pubSubProvider->publish([ 'type' => 'metrics', 'payload' => serialize($metric), ])->then(function ($total) use ($metric) { $metric->setSubscriberCount($total); }); } /** * Merge the given metrics into a single result set. */ protected function mergeSubscriberMetrics(array $metrics, MetricType $type): array { return match ($type) { MetricType::CONNECTIONS => array_reduce($metrics, fn ($carry, $item) => array_merge($carry, $item), []), MetricType::CHANNELS => $this->mergeChannels($metrics), MetricType::CHANNEL => $this->mergeChannel($metrics), MetricType::CHANNEL_USERS => collect($metrics)->flatten(1)->unique()->all(), default => [], }; } /** * Merge multiple channel instances into a single set. */ protected function mergeChannel(array $metrics): array { return collect($metrics) ->reduce(function ($carry, $item) { collect($item)->each(fn ($value, $key) => $carry->put($key, match ($key) { 'occupied' => $carry->get($key, false) || $value, 'user_count' => $carry->get($key, 0) + $value, 'subscription_count' => $carry->get($key, 0) + $value, default => $value, })); return $carry; }, collect()) ->all(); } /** * Merge multiple sets of channel instances into a single result set. */ protected function mergeChannels(array $metrics): array { return collect($metrics) ->reduce(function ($carry, $item) { collect($item)->each(function ($data, $channel) use ($carry) { $metrics = $carry->get($channel, []); $metrics[] = $data; $carry->put($channel, $metrics); }); return $carry; }, collect()) ->map(fn ($metrics) => $this->mergeChannel($metrics)) ->all(); } /** * Listen for metrics from subscribers. */ protected function listenForMetrics(PendingMetric $metric): Deferred { $deferred = new Deferred; $this->pubSubProvider->on($metric->key(), function ($payload) use ($metric, $deferred) { $pending = $this->metrics[$metric->key()]; $pending->append($payload['payload']); if ($pending->resolvable()) { $deferred->resolve($pending->resolve()); } }); return $deferred; } /** * Publish the metrics for the given type. */ public function publish(PendingMetric $metric): void { $this->pubSubProvider->publish([ 'type' => $metric->key(), 'payload' => $this->get($metric), ]); } /** * Stop listening for the given metric. */ protected function stopListening(PendingMetric $metric): void { unset($this->metrics[$metric->key()]); $this->pubSubProvider->stopListening($metric->key()); } /** * Create a promise to resolve the given value. */ protected function promise(mixed $value): PromiseInterface { $deferred = new Deferred; $promise = $deferred->promise(); $deferred->resolve($value); return $promise; } }