├── .github └── workflows │ └── ci.yml ├── ClientMonitoringExtension.php ├── ConsumedMessageStats.php ├── ConsumerMonitoringExtension.php ├── ConsumerStats.php ├── DatadogStorage.php ├── GenericStatsStorageFactory.php ├── InfluxDbStorage.php ├── JsonSerializer.php ├── LICENSE ├── README.md ├── Resources.php ├── SentMessageStats.php ├── Serializer.php ├── Stats.php ├── StatsStorage.php ├── StatsStorageFactory.php ├── Symfony └── DependencyInjection │ └── MonitoringFactory.php ├── WampStorage.php └── composer.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | jobs: 8 | tests: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | php: ['7.4', '8.0', '8.1', '8.2'] 14 | 15 | name: PHP ${{ matrix.php }} tests 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: ${{ matrix.php }} 23 | coverage: none 24 | 25 | - uses: "ramsey/composer-install@v1" 26 | 27 | - run: vendor/bin/phpunit --exclude-group=functional 28 | -------------------------------------------------------------------------------- /ClientMonitoringExtension.php: -------------------------------------------------------------------------------- 1 | storage = $storage; 27 | $this->logger = $logger; 28 | } 29 | 30 | public function onPostSend(PostSend $context): void 31 | { 32 | $timestampMs = (int) (microtime(true) * 1000); 33 | 34 | $destination = $context->getTransportDestination() instanceof Topic 35 | ? $context->getTransportDestination()->getTopicName() 36 | : $context->getTransportDestination()->getQueueName() 37 | ; 38 | 39 | $stats = new SentMessageStats( 40 | $timestampMs, 41 | $destination, 42 | $context->getTransportDestination() instanceof Topic, 43 | $context->getTransportMessage()->getMessageId(), 44 | $context->getTransportMessage()->getCorrelationId(), 45 | $context->getTransportMessage()->getHeaders(), 46 | $context->getTransportMessage()->getProperties() 47 | ); 48 | 49 | $this->safeCall(function () use ($stats) { 50 | $this->storage->pushSentMessageStats($stats); 51 | }); 52 | } 53 | 54 | private function safeCall(callable $fun) 55 | { 56 | try { 57 | return call_user_func($fun); 58 | } catch (\Throwable $e) { 59 | $this->logger->error(sprintf('[ClientMonitoringExtension] Push to storage failed: %s', $e->getMessage())); 60 | } 61 | 62 | return null; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /ConsumedMessageStats.php: -------------------------------------------------------------------------------- 1 | consumerId = $consumerId; 113 | $this->timestampMs = $timestampMs; 114 | $this->receivedAtMs = $receivedAtMs; 115 | $this->queue = $queue; 116 | $this->messageId = $messageId; 117 | $this->correlationId = $correlationId; 118 | $this->headers = $headers; 119 | $this->properties = $properties; 120 | $this->redelivered = $redelivered; 121 | $this->status = $status; 122 | 123 | $this->errorClass = $errorClass; 124 | $this->errorMessage = $errorMessage; 125 | $this->errorCode = $errorCode; 126 | $this->errorFile = $errorFile; 127 | $this->errorLine = $errorLine; 128 | $this->trance = $trace; 129 | } 130 | 131 | public function getConsumerId(): string 132 | { 133 | return $this->consumerId; 134 | } 135 | 136 | public function getTimestampMs(): int 137 | { 138 | return $this->timestampMs; 139 | } 140 | 141 | public function getReceivedAtMs(): int 142 | { 143 | return $this->receivedAtMs; 144 | } 145 | 146 | public function getQueue(): string 147 | { 148 | return $this->queue; 149 | } 150 | 151 | public function getMessageId(): ?string 152 | { 153 | return $this->messageId; 154 | } 155 | 156 | public function getCorrelationId(): ?string 157 | { 158 | return $this->correlationId; 159 | } 160 | 161 | public function getHeaders(): array 162 | { 163 | return $this->headers; 164 | } 165 | 166 | public function getProperties(): array 167 | { 168 | return $this->properties; 169 | } 170 | 171 | public function isRedelivered(): bool 172 | { 173 | return $this->redelivered; 174 | } 175 | 176 | public function getStatus(): string 177 | { 178 | return $this->status; 179 | } 180 | 181 | public function getErrorClass(): ?string 182 | { 183 | return $this->errorClass; 184 | } 185 | 186 | public function getErrorMessage(): ?string 187 | { 188 | return $this->errorMessage; 189 | } 190 | 191 | public function getErrorCode(): ?int 192 | { 193 | return $this->errorCode; 194 | } 195 | 196 | public function getErrorFile(): ?string 197 | { 198 | return $this->errorFile; 199 | } 200 | 201 | public function getErrorLine(): ?int 202 | { 203 | return $this->errorLine; 204 | } 205 | 206 | public function getTrance(): ?string 207 | { 208 | return $this->trance; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /ConsumerMonitoringExtension.php: -------------------------------------------------------------------------------- 1 | storage = $storage; 80 | $this->updateStatsPeriod = 60; 81 | } 82 | 83 | public function onStart(Start $context): void 84 | { 85 | $this->consumerId = UUID::generate(); 86 | 87 | $this->queues = []; 88 | 89 | $this->startedAtMs = 0; 90 | $this->lastStatsAt = 0; 91 | 92 | $this->received = 0; 93 | $this->acknowledged = 0; 94 | $this->rejected = 0; 95 | $this->requeued = 0; 96 | } 97 | 98 | public function onPreSubscribe(PreSubscribe $context): void 99 | { 100 | $this->queues[] = $context->getConsumer()->getQueue()->getQueueName(); 101 | } 102 | 103 | public function onPreConsume(PreConsume $context): void 104 | { 105 | // send started only once 106 | $isStarted = false; 107 | if (0 === $this->startedAtMs) { 108 | $isStarted = true; 109 | $this->startedAtMs = $context->getStartTime(); 110 | } 111 | 112 | // send stats event only once per period 113 | $time = time(); 114 | if (($time - $this->lastStatsAt) > $this->updateStatsPeriod) { 115 | $this->lastStatsAt = $time; 116 | 117 | $event = new ConsumerStats( 118 | $this->consumerId, 119 | $this->getNowMs(), 120 | $this->startedAtMs, 121 | null, 122 | $isStarted, 123 | false, 124 | false, 125 | $this->queues, 126 | $this->received, 127 | $this->acknowledged, 128 | $this->rejected, 129 | $this->requeued, 130 | $this->getMemoryUsage(), 131 | $this->getSystemLoad() 132 | ); 133 | 134 | $this->safeCall(function () use ($event) { 135 | $this->storage->pushConsumerStats($event); 136 | }, $context->getLogger()); 137 | } 138 | } 139 | 140 | public function onEnd(End $context): void 141 | { 142 | $event = new ConsumerStats( 143 | $this->consumerId, 144 | $this->getNowMs(), 145 | $this->startedAtMs, 146 | $context->getEndTime(), 147 | false, 148 | true, 149 | false, 150 | $this->queues, 151 | $this->received, 152 | $this->acknowledged, 153 | $this->rejected, 154 | $this->requeued, 155 | $this->getMemoryUsage(), 156 | $this->getSystemLoad() 157 | ); 158 | 159 | $this->safeCall(function () use ($event) { 160 | $this->storage->pushConsumerStats($event); 161 | }, $context->getLogger()); 162 | } 163 | 164 | public function onProcessorException(ProcessorException $context): void 165 | { 166 | $timeMs = $this->getNowMs(); 167 | 168 | $event = new ConsumedMessageStats( 169 | $this->consumerId, 170 | $timeMs, 171 | $context->getReceivedAt(), 172 | $context->getConsumer()->getQueue()->getQueueName(), 173 | $context->getMessage()->getMessageId(), 174 | $context->getMessage()->getCorrelationId(), 175 | $context->getMessage()->getHeaders(), 176 | $context->getMessage()->getProperties(), 177 | $context->getMessage()->isRedelivered(), 178 | ConsumedMessageStats::STATUS_FAILED, 179 | get_class($context->getException()), 180 | $context->getException()->getMessage(), 181 | $context->getException()->getCode(), 182 | $context->getException()->getFile(), 183 | $context->getException()->getLine(), 184 | $context->getException()->getTraceAsString() 185 | ); 186 | 187 | $this->safeCall(function () use ($event) { 188 | $this->storage->pushConsumedMessageStats($event); 189 | }, $context->getLogger()); 190 | 191 | // priority of this extension must be the lowest and 192 | // if result is null we emit consumer stopped event here 193 | if (null === $context->getResult()) { 194 | $event = new ConsumerStats( 195 | $this->consumerId, 196 | $timeMs, 197 | $this->startedAtMs, 198 | $timeMs, 199 | false, 200 | true, 201 | true, 202 | $this->queues, 203 | $this->received, 204 | $this->acknowledged, 205 | $this->rejected, 206 | $this->requeued, 207 | $this->getMemoryUsage(), 208 | $this->getSystemLoad(), 209 | get_class($context->getException()), 210 | $context->getException()->getMessage(), 211 | $context->getException()->getCode(), 212 | $context->getException()->getFile(), 213 | $context->getException()->getLine(), 214 | $context->getException()->getTraceAsString() 215 | ); 216 | 217 | $this->safeCall(function () use ($event) { 218 | $this->storage->pushConsumerStats($event); 219 | }, $context->getLogger()); 220 | } 221 | } 222 | 223 | public function onMessageReceived(MessageReceived $context): void 224 | { 225 | ++$this->received; 226 | } 227 | 228 | public function onResult(MessageResult $context): void 229 | { 230 | $timeMs = $this->getNowMs(); 231 | 232 | switch ($context->getResult()) { 233 | case Result::ACK: 234 | case Result::ALREADY_ACKNOWLEDGED: 235 | $this->acknowledged++; 236 | $status = ConsumedMessageStats::STATUS_ACK; 237 | break; 238 | case Result::REJECT: 239 | $this->rejected++; 240 | $status = ConsumedMessageStats::STATUS_REJECTED; 241 | break; 242 | case Result::REQUEUE: 243 | $this->requeued++; 244 | $status = ConsumedMessageStats::STATUS_REQUEUED; 245 | break; 246 | default: 247 | throw new \LogicException(); 248 | } 249 | 250 | $event = new ConsumedMessageStats( 251 | $this->consumerId, 252 | $timeMs, 253 | $context->getReceivedAt(), 254 | $context->getConsumer()->getQueue()->getQueueName(), 255 | $context->getMessage()->getMessageId(), 256 | $context->getMessage()->getCorrelationId(), 257 | $context->getMessage()->getHeaders(), 258 | $context->getMessage()->getProperties(), 259 | $context->getMessage()->isRedelivered(), 260 | $status 261 | ); 262 | 263 | $this->safeCall(function () use ($event) { 264 | $this->storage->pushConsumedMessageStats($event); 265 | }, $context->getLogger()); 266 | 267 | // send stats event only once per period 268 | $time = time(); 269 | if (($time - $this->lastStatsAt) > $this->updateStatsPeriod) { 270 | $this->lastStatsAt = $time; 271 | 272 | $event = new ConsumerStats( 273 | $this->consumerId, 274 | $timeMs, 275 | $this->startedAtMs, 276 | null, 277 | false, 278 | false, 279 | false, 280 | $this->queues, 281 | $this->received, 282 | $this->acknowledged, 283 | $this->rejected, 284 | $this->requeued, 285 | $this->getMemoryUsage(), 286 | $this->getSystemLoad() 287 | ); 288 | 289 | $this->safeCall(function () use ($event) { 290 | $this->storage->pushConsumerStats($event); 291 | }, $context->getLogger()); 292 | } 293 | } 294 | 295 | private function getNowMs(): int 296 | { 297 | return (int) (microtime(true) * 1000); 298 | } 299 | 300 | private function getMemoryUsage(): int 301 | { 302 | return memory_get_usage(true); 303 | } 304 | 305 | private function getSystemLoad(): float 306 | { 307 | return sys_getloadavg()[0]; 308 | } 309 | 310 | private function safeCall(callable $fun, LoggerInterface $logger) 311 | { 312 | try { 313 | return call_user_func($fun); 314 | } catch (\Throwable $e) { 315 | $logger->error(sprintf('[ConsumerMonitoringExtension] Push to storage failed: %s', $e->getMessage())); 316 | } 317 | 318 | return null; 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /ConsumerStats.php: -------------------------------------------------------------------------------- 1 | consumerId = $consumerId; 132 | $this->timestampMs = $timestampMs; 133 | $this->startedAtMs = $startedAtMs; 134 | $this->finishedAtMs = $finishedAtMs; 135 | 136 | $this->started = $started; 137 | $this->finished = $finished; 138 | $this->failed = $failed; 139 | 140 | $this->queues = $queues; 141 | $this->startedAtMs = $startedAtMs; 142 | $this->received = $received; 143 | $this->acknowledged = $acknowledged; 144 | $this->rejected = $rejected; 145 | $this->requeued = $requeued; 146 | 147 | $this->memoryUsage = $memoryUsage; 148 | $this->systemLoad = $systemLoad; 149 | 150 | $this->errorClass = $errorClass; 151 | $this->errorMessage = $errorMessage; 152 | $this->errorCode = $errorCode; 153 | $this->errorFile = $errorFile; 154 | $this->errorLine = $errorLine; 155 | $this->trance = $trace; 156 | } 157 | 158 | public function getConsumerId(): string 159 | { 160 | return $this->consumerId; 161 | } 162 | 163 | public function getTimestampMs(): int 164 | { 165 | return $this->timestampMs; 166 | } 167 | 168 | public function getStartedAtMs(): int 169 | { 170 | return $this->startedAtMs; 171 | } 172 | 173 | public function getFinishedAtMs(): ?int 174 | { 175 | return $this->finishedAtMs; 176 | } 177 | 178 | public function isStarted(): bool 179 | { 180 | return $this->started; 181 | } 182 | 183 | public function isFinished(): bool 184 | { 185 | return $this->finished; 186 | } 187 | 188 | public function isFailed(): bool 189 | { 190 | return $this->failed; 191 | } 192 | 193 | public function getQueues(): array 194 | { 195 | return $this->queues; 196 | } 197 | 198 | public function getReceived(): int 199 | { 200 | return $this->received; 201 | } 202 | 203 | public function getAcknowledged(): int 204 | { 205 | return $this->acknowledged; 206 | } 207 | 208 | public function getRejected(): int 209 | { 210 | return $this->rejected; 211 | } 212 | 213 | public function getRequeued(): int 214 | { 215 | return $this->requeued; 216 | } 217 | 218 | public function getMemoryUsage(): int 219 | { 220 | return $this->memoryUsage; 221 | } 222 | 223 | public function getSystemLoad(): float 224 | { 225 | return $this->systemLoad; 226 | } 227 | 228 | public function getErrorClass(): ?string 229 | { 230 | return $this->errorClass; 231 | } 232 | 233 | public function getErrorMessage(): ?string 234 | { 235 | return $this->errorMessage; 236 | } 237 | 238 | public function getErrorCode(): ?int 239 | { 240 | return $this->errorCode; 241 | } 242 | 243 | public function getErrorFile(): ?string 244 | { 245 | return $this->errorFile; 246 | } 247 | 248 | public function getErrorLine(): ?int 249 | { 250 | return $this->errorLine; 251 | } 252 | 253 | public function getTrance(): ?string 254 | { 255 | return $this->trance; 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /DatadogStorage.php: -------------------------------------------------------------------------------- 1 | config = $this->prepareConfig($config); 31 | 32 | if (null === $this->datadog) { 33 | if (true === filter_var($this->config['batched'], \FILTER_VALIDATE_BOOLEAN)) { 34 | $this->datadog = new BatchedDogStatsd($this->config); 35 | } else { 36 | $this->datadog = new DogStatsd($this->config); 37 | } 38 | } 39 | } 40 | 41 | public function pushConsumerStats(ConsumerStats $stats): void 42 | { 43 | $queues = $stats->getQueues(); 44 | array_walk($queues, function (string $queue) use ($stats) { 45 | $tags = [ 46 | 'queue' => $queue, 47 | 'consumerId' => $stats->getConsumerId(), 48 | ]; 49 | 50 | if ($stats->getFinishedAtMs()) { 51 | $values['finishedAtMs'] = $stats->getFinishedAtMs(); 52 | } 53 | 54 | $this->datadog->gauge($this->config['metric.consumers.started'], (int) $stats->isStarted(), 1, $tags); 55 | $this->datadog->gauge($this->config['metric.consumers.finished'], (int) $stats->isFinished(), 1, $tags); 56 | $this->datadog->gauge($this->config['metric.consumers.failed'], (int) $stats->isFailed(), 1, $tags); 57 | $this->datadog->gauge($this->config['metric.consumers.received'], $stats->getReceived(), 1, $tags); 58 | $this->datadog->gauge($this->config['metric.consumers.acknowledged'], $stats->getAcknowledged(), 1, $tags); 59 | $this->datadog->gauge($this->config['metric.consumers.rejected'], $stats->getRejected(), 1, $tags); 60 | $this->datadog->gauge($this->config['metric.consumers.requeued'], $stats->getRejected(), 1, $tags); 61 | $this->datadog->gauge($this->config['metric.consumers.memoryUsage'], $stats->getMemoryUsage(), 1, $tags); 62 | }); 63 | } 64 | 65 | public function pushSentMessageStats(SentMessageStats $stats): void 66 | { 67 | $tags = [ 68 | 'destination' => $stats->getDestination(), 69 | ]; 70 | 71 | $properties = $stats->getProperties(); 72 | if (false === empty($properties[Config::TOPIC])) { 73 | $tags['topic'] = $properties[Config::TOPIC]; 74 | } 75 | 76 | if (false === empty($properties[Config::COMMAND])) { 77 | $tags['command'] = $properties[Config::COMMAND]; 78 | } 79 | 80 | $this->datadog->increment($this->config['metric.messages.sent'], 1, $tags); 81 | } 82 | 83 | public function pushConsumedMessageStats(ConsumedMessageStats $stats): void 84 | { 85 | $tags = [ 86 | 'queue' => $stats->getQueue(), 87 | 'status' => $stats->getStatus(), 88 | ]; 89 | 90 | if (ConsumedMessageStats::STATUS_FAILED === $stats->getStatus()) { 91 | $this->datadog->increment($this->config['metric.messages.failed'], 1, $tags); 92 | } 93 | 94 | if ($stats->isRedelivered()) { 95 | $this->datadog->increment($this->config['metric.messages.redelivered'], 1, $tags); 96 | } 97 | 98 | $runtime = $stats->getTimestampMs() - $stats->getReceivedAtMs(); 99 | $this->datadog->histogram($this->config['metric.messages.consumed'], $runtime, 1, $tags); 100 | } 101 | 102 | private function parseDsn(string $dsn): array 103 | { 104 | $dsn = Dsn::parseFirst($dsn); 105 | 106 | if ('datadog' !== $dsn->getSchemeProtocol()) { 107 | throw new \LogicException(sprintf('The given scheme protocol "%s" is not supported. It must be "datadog"', $dsn->getSchemeProtocol())); 108 | } 109 | 110 | return array_filter(array_replace($dsn->getQuery(), [ 111 | 'host' => $dsn->getHost(), 112 | 'port' => $dsn->getPort(), 113 | 'global_tags' => $dsn->getString('global_tags'), 114 | 'batched' => $dsn->getString('batched'), 115 | 'metric.messages.sent' => $dsn->getString('metric.messages.sent'), 116 | 'metric.messages.consumed' => $dsn->getString('metric.messages.consumed'), 117 | 'metric.messages.redelivered' => $dsn->getString('metric.messages.redelivered'), 118 | 'metric.messages.failed' => $dsn->getString('metric.messages.failed'), 119 | 'metric.consumers.started' => $dsn->getString('metric.consumers.started'), 120 | 'metric.consumers.finished' => $dsn->getString('metric.consumers.finished'), 121 | 'metric.consumers.failed' => $dsn->getString('metric.consumers.failed'), 122 | 'metric.consumers.received' => $dsn->getString('metric.consumers.received'), 123 | 'metric.consumers.acknowledged' => $dsn->getString('metric.consumers.acknowledged'), 124 | 'metric.consumers.rejected' => $dsn->getString('metric.consumers.rejected'), 125 | 'metric.consumers.requeued' => $dsn->getString('metric.consumers.requeued'), 126 | 'metric.consumers.memoryUsage' => $dsn->getString('metric.consumers.memoryUsage'), 127 | ]), function ($value) { 128 | return null !== $value; 129 | }); 130 | } 131 | 132 | private function prepareConfig($config): array 133 | { 134 | if (empty($config)) { 135 | $config = $this->parseDsn('datadog:'); 136 | } elseif (\is_string($config)) { 137 | $config = $this->parseDsn($config); 138 | } elseif (\is_array($config)) { 139 | $config = empty($config['dsn']) ? $config : $this->parseDsn($config['dsn']); 140 | } elseif ($config instanceof DogStatsd) { 141 | $this->datadog = $config; 142 | $config = []; 143 | } else { 144 | throw new \LogicException('The config must be either an array of options, a DSN string or null'); 145 | } 146 | 147 | return array_replace([ 148 | 'host' => 'localhost', 149 | 'port' => 8125, 150 | 'batched' => true, 151 | 'metric.messages.sent' => 'enqueue.messages.sent', 152 | 'metric.messages.consumed' => 'enqueue.messages.consumed', 153 | 'metric.messages.redelivered' => 'enqueue.messages.redelivered', 154 | 'metric.messages.failed' => 'enqueue.messages.failed', 155 | 'metric.consumers.started' => 'enqueue.consumers.started', 156 | 'metric.consumers.finished' => 'enqueue.consumers.finished', 157 | 'metric.consumers.failed' => 'enqueue.consumers.failed', 158 | 'metric.consumers.received' => 'enqueue.consumers.received', 159 | 'metric.consumers.acknowledged' => 'enqueue.consumers.acknowledged', 160 | 'metric.consumers.rejected' => 'enqueue.consumers.rejected', 161 | 'metric.consumers.requeued' => 'enqueue.consumers.requeued', 162 | 'metric.consumers.memoryUsage' => 'enqueue.consumers.memoryUsage', 163 | ], $config); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /GenericStatsStorageFactory.php: -------------------------------------------------------------------------------- 1 | $config]; 15 | } 16 | 17 | if (false === \is_array($config)) { 18 | throw new \InvalidArgumentException('The config must be either array or DSN string.'); 19 | } 20 | 21 | if (false === array_key_exists('dsn', $config)) { 22 | throw new \InvalidArgumentException('The config must have dsn key set.'); 23 | } 24 | 25 | $dsn = Dsn::parseFirst($config['dsn']); 26 | 27 | if ($storageClass = $this->findStorageClass($dsn, Resources::getKnownStorages())) { 28 | return new $storageClass(1 === \count($config) ? $config['dsn'] : $config); 29 | } 30 | 31 | throw new \LogicException(sprintf('A given scheme "%s" is not supported.', $dsn->getScheme())); 32 | } 33 | 34 | private function findStorageClass(Dsn $dsn, array $factories): ?string 35 | { 36 | $protocol = $dsn->getSchemeProtocol(); 37 | 38 | if ($dsn->getSchemeExtensions()) { 39 | foreach ($factories as $storageClass => $info) { 40 | if (empty($info['supportedSchemeExtensions'])) { 41 | continue; 42 | } 43 | 44 | if (false === \in_array($protocol, $info['schemes'], true)) { 45 | continue; 46 | } 47 | 48 | $diff = array_diff($info['supportedSchemeExtensions'], $dsn->getSchemeExtensions()); 49 | if (empty($diff)) { 50 | return $storageClass; 51 | } 52 | } 53 | } 54 | 55 | foreach ($factories as $storageClass => $info) { 56 | if (false === \in_array($protocol, $info['schemes'], true)) { 57 | continue; 58 | } 59 | 60 | return $storageClass; 61 | } 62 | 63 | return null; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /InfluxDbStorage.php: -------------------------------------------------------------------------------- 1 | 'influxdb://127.0.0.1:8086', 35 | * 'host' => '127.0.0.1', 36 | * 'port' => '8086', 37 | * 'user' => '', 38 | * 'password' => '', 39 | * 'db' => 'enqueue', 40 | * 'measurementSentMessages' => 'sent-messages', 41 | * 'measurementConsumedMessages' => 'consumed-messages', 42 | * 'measurementConsumers' => 'consumers', 43 | * 'client' => null, # Client instance. Null by default. 44 | * 'retentionPolicy' => null, 45 | * ] 46 | * 47 | * or 48 | * 49 | * influxdb://127.0.0.1:8086?user=Jon&password=secret 50 | * 51 | * @param array|string|null $config 52 | */ 53 | public function __construct($config = 'influxdb:') 54 | { 55 | if (false == class_exists(Client::class)) { 56 | throw new \LogicException('Seems client library is not installed. Please install "influxdb/influxdb-php"'); 57 | } 58 | 59 | if (empty($config)) { 60 | $config = []; 61 | } elseif (is_string($config)) { 62 | $config = self::parseDsn($config); 63 | } elseif (is_array($config)) { 64 | $config = empty($config['dsn']) ? $config : self::parseDsn($config['dsn']); 65 | } elseif ($config instanceof Client) { 66 | // Passing Client instead of array config is deprecated because it prevents setting any configuration values 67 | // and causes library to use defaults. 68 | @trigger_error( 69 | sprintf('Passing %s as %s argument is deprecated. Pass it as "client" array property or use createWithClient instead', 70 | Client::class, 71 | __METHOD__ 72 | ), \E_USER_DEPRECATED); 73 | $this->client = $config; 74 | $config = []; 75 | } else { 76 | throw new \LogicException('The config must be either an array of options, a DSN string or null'); 77 | } 78 | 79 | $config = array_replace([ 80 | 'host' => '127.0.0.1', 81 | 'port' => '8086', 82 | 'user' => '', 83 | 'password' => '', 84 | 'db' => 'enqueue', 85 | 'measurementSentMessages' => 'sent-messages', 86 | 'measurementConsumedMessages' => 'consumed-messages', 87 | 'measurementConsumers' => 'consumers', 88 | 'client' => null, 89 | 'retentionPolicy' => null, 90 | ], $config); 91 | 92 | if (null !== $config['client']) { 93 | if (!$config['client'] instanceof Client) { 94 | throw new \InvalidArgumentException(sprintf('%s configuration property is expected to be an instance of %s class. %s was passed instead.', 'client', Client::class, gettype($config['client']))); 95 | } 96 | $this->client = $config['client']; 97 | } 98 | 99 | $this->config = $config; 100 | } 101 | 102 | /** 103 | * @param string $config 104 | */ 105 | public static function createWithClient(Client $client, $config = 'influxdb:'): self 106 | { 107 | if (is_string($config)) { 108 | $config = self::parseDsn($config); 109 | } 110 | $config['client'] = $client; 111 | 112 | return new self($config); 113 | } 114 | 115 | public function pushConsumerStats(ConsumerStats $stats): void 116 | { 117 | $points = []; 118 | 119 | foreach ($stats->getQueues() as $queue) { 120 | $tags = [ 121 | 'queue' => $queue, 122 | 'consumerId' => $stats->getConsumerId(), 123 | ]; 124 | 125 | $values = [ 126 | 'startedAtMs' => $stats->getStartedAtMs(), 127 | 'started' => $stats->isStarted(), 128 | 'finished' => $stats->isFinished(), 129 | 'failed' => $stats->isFailed(), 130 | 'received' => $stats->getReceived(), 131 | 'acknowledged' => $stats->getAcknowledged(), 132 | 'rejected' => $stats->getRejected(), 133 | 'requeued' => $stats->getRequeued(), 134 | 'memoryUsage' => $stats->getMemoryUsage(), 135 | 'systemLoad' => $stats->getSystemLoad(), 136 | ]; 137 | 138 | if ($stats->getFinishedAtMs()) { 139 | $values['finishedAtMs'] = $stats->getFinishedAtMs(); 140 | } 141 | 142 | $points[] = new Point($this->config['measurementConsumers'], null, $tags, $values, $stats->getTimestampMs()); 143 | } 144 | 145 | $this->doWrite($points); 146 | } 147 | 148 | public function pushConsumedMessageStats(ConsumedMessageStats $stats): void 149 | { 150 | $tags = [ 151 | 'queue' => $stats->getQueue(), 152 | 'status' => $stats->getStatus(), 153 | ]; 154 | 155 | $properties = $stats->getProperties(); 156 | 157 | if (false === empty($properties[Config::TOPIC])) { 158 | $tags['topic'] = $properties[Config::TOPIC]; 159 | } 160 | 161 | if (false === empty($properties[Config::COMMAND])) { 162 | $tags['command'] = $properties[Config::COMMAND]; 163 | } 164 | 165 | $values = [ 166 | 'receivedAt' => $stats->getReceivedAtMs(), 167 | 'processedAt' => $stats->getTimestampMs(), 168 | 'redelivered' => $stats->isRedelivered(), 169 | ]; 170 | 171 | if (ConsumedMessageStats::STATUS_FAILED === $stats->getStatus()) { 172 | $values['failed'] = 1; 173 | } 174 | 175 | $runtime = $stats->getTimestampMs() - $stats->getReceivedAtMs(); 176 | 177 | $points = [ 178 | new Point($this->config['measurementConsumedMessages'], $runtime, $tags, $values, $stats->getTimestampMs()), 179 | ]; 180 | 181 | $this->doWrite($points); 182 | } 183 | 184 | public function pushSentMessageStats(SentMessageStats $stats): void 185 | { 186 | $tags = [ 187 | 'destination' => $stats->getDestination(), 188 | ]; 189 | 190 | $properties = $stats->getProperties(); 191 | 192 | if (false === empty($properties[Config::TOPIC])) { 193 | $tags['topic'] = $properties[Config::TOPIC]; 194 | } 195 | 196 | if (false === empty($properties[Config::COMMAND])) { 197 | $tags['command'] = $properties[Config::COMMAND]; 198 | } 199 | 200 | $points = [ 201 | new Point($this->config['measurementSentMessages'], 1, $tags, [], $stats->getTimestampMs()), 202 | ]; 203 | 204 | $this->doWrite($points); 205 | } 206 | 207 | private function doWrite(array $points): void 208 | { 209 | if (null === $this->client) { 210 | $this->client = new Client( 211 | $this->config['host'], 212 | $this->config['port'], 213 | $this->config['user'], 214 | $this->config['password'] 215 | ); 216 | } 217 | 218 | if ($this->client->getDriver() instanceof QueryDriverInterface) { 219 | if (null === $this->database) { 220 | $this->database = $this->client->selectDB($this->config['db']); 221 | $this->database->create(); 222 | } 223 | 224 | $this->database->writePoints($points, Database::PRECISION_MILLISECONDS, $this->config['retentionPolicy']); 225 | } else { 226 | // Code below mirrors what `writePoints` method of Database does. 227 | try { 228 | $parameters = [ 229 | 'url' => sprintf('write?db=%s&precision=%s', $this->config['db'], Database::PRECISION_MILLISECONDS), 230 | 'database' => $this->config['db'], 231 | 'method' => 'post', 232 | ]; 233 | if (null !== $this->config['retentionPolicy']) { 234 | $parameters['url'] .= sprintf('&rp=%s', $this->config['retentionPolicy']); 235 | } 236 | 237 | $this->client->write($parameters, $points); 238 | } catch (\Exception $e) { 239 | throw new InfluxDBException($e->getMessage(), $e->getCode()); 240 | } 241 | } 242 | } 243 | 244 | private static function parseDsn(string $dsn): array 245 | { 246 | $dsn = Dsn::parseFirst($dsn); 247 | 248 | if (false === in_array($dsn->getSchemeProtocol(), ['influxdb'], true)) { 249 | throw new \LogicException(sprintf('The given scheme protocol "%s" is not supported. It must be "influxdb"', $dsn->getSchemeProtocol())); 250 | } 251 | 252 | return array_filter(array_replace($dsn->getQuery(), [ 253 | 'host' => $dsn->getHost(), 254 | 'port' => $dsn->getPort(), 255 | 'user' => $dsn->getUser(), 256 | 'password' => $dsn->getPassword(), 257 | 'db' => $dsn->getString('db'), 258 | 'measurementSentMessages' => $dsn->getString('measurementSentMessages'), 259 | 'measurementConsumedMessages' => $dsn->getString('measurementConsumedMessages'), 260 | 'measurementConsumers' => $dsn->getString('measurementConsumers'), 261 | 'retentionPolicy' => $dsn->getString('retentionPolicy'), 262 | ]), function ($value) { return null !== $value; }); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /JsonSerializer.php: -------------------------------------------------------------------------------- 1 | $rfClass->getShortName(), 15 | ]; 16 | 17 | foreach ($rfClass->getProperties() as $rfProperty) { 18 | $rfProperty->setAccessible(true); 19 | $data[$rfProperty->getName()] = $rfProperty->getValue($stats); 20 | $rfProperty->setAccessible(false); 21 | } 22 | 23 | $json = json_encode($data); 24 | 25 | if (\JSON_ERROR_NONE !== json_last_error()) { 26 | throw new \InvalidArgumentException(sprintf('The malformed json given. Error %s and message %s', json_last_error(), json_last_error_msg())); 27 | } 28 | 29 | return $json; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2018 Forma-Pro 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is furnished 9 | to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Supporting Enqueue

2 | 3 | Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: 4 | 5 | - [Become a sponsor](https://www.patreon.com/makasim) 6 | - [Become our client](http://forma-pro.com/) 7 | 8 | --- 9 | 10 | # Enqueue Monitoring 11 | 12 | Queue Monitoring tool. Track sent, consumed messages. Consumers performances. 13 | 14 | * Could be used with any message queue library. 15 | * Could be integrated to any PHP framework 16 | * Could send stats to any analytical platform 17 | * Supports Datadog, InfluxDb, Grafana and WAMP out of the box. 18 | * Provides integration for Enqueue 19 | 20 | [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) 21 | [![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/monitoring/ci.yml?branch=master)](https://github.com/php-enqueue/monitoring/actions?query=workflow%3ACI) 22 | [![Total Downloads](https://poser.pugx.org/enqueue/monitoring/d/total.png)](https://packagist.org/packages/enqueue/monitoring) 23 | [![Latest Stable Version](https://poser.pugx.org/enqueue/monitoring/version.png)](https://packagist.org/packages/enqueue/monitoring) 24 | 25 | ## Resources 26 | 27 | * [Site](https://enqueue.forma-pro.com/) 28 | * [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/monitoring.md) 29 | * [Questions](https://gitter.im/php-enqueue/Lobby) 30 | * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) 31 | 32 | ## Developed by Forma-Pro 33 | 34 | Forma-Pro is a full stack development company which interests also spread to open source development. 35 | Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. 36 | Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. 37 | 38 | If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com 39 | 40 | ## License 41 | 42 | It is released under the [MIT License](LICENSE). 43 | -------------------------------------------------------------------------------- /Resources.php: -------------------------------------------------------------------------------- 1 | $item) { 22 | foreach ($item['schemes'] as $scheme) { 23 | $schemes[$scheme] = $storageClass; 24 | } 25 | } 26 | 27 | return $schemes; 28 | } 29 | 30 | public static function getKnownStorages(): array 31 | { 32 | if (null === self::$knownStorages) { 33 | $map = []; 34 | 35 | $map[WampStorage::class] = [ 36 | 'schemes' => ['wamp', 'ws'], 37 | 'supportedSchemeExtensions' => [], 38 | ]; 39 | 40 | $map[InfluxDbStorage::class] = [ 41 | 'schemes' => ['influxdb'], 42 | 'supportedSchemeExtensions' => [], 43 | ]; 44 | 45 | $map[DatadogStorage::class] = [ 46 | 'schemes' => ['datadog'], 47 | 'supportedSchemeExtensions' => [], 48 | ]; 49 | 50 | self::$knownStorages = $map; 51 | } 52 | 53 | return self::$knownStorages; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /SentMessageStats.php: -------------------------------------------------------------------------------- 1 | timestampMs = $timestampMs; 54 | $this->destination = $destination; 55 | $this->isTopic = $isTopic; 56 | $this->messageId = $messageId; 57 | $this->correlationId = $correlationId; 58 | $this->headers = $headers; 59 | $this->properties = $properties; 60 | } 61 | 62 | public function getTimestampMs(): int 63 | { 64 | return $this->timestampMs; 65 | } 66 | 67 | public function getDestination(): string 68 | { 69 | return $this->destination; 70 | } 71 | 72 | public function isTopic(): bool 73 | { 74 | return $this->isTopic; 75 | } 76 | 77 | public function getMessageId(): ?string 78 | { 79 | return $this->messageId; 80 | } 81 | 82 | public function getCorrelationId(): ?string 83 | { 84 | return $this->correlationId; 85 | } 86 | 87 | public function getHeaders(): array 88 | { 89 | return $this->headers; 90 | } 91 | 92 | public function getProperties(): array 93 | { 94 | return $this->properties; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Serializer.php: -------------------------------------------------------------------------------- 1 | diUtils = DiUtils::create(self::MODULE, $name); 35 | } 36 | 37 | public static function getConfiguration(string $name = 'monitoring'): ArrayNodeDefinition 38 | { 39 | $builder = new ArrayNodeDefinition($name); 40 | 41 | $builder 42 | ->info(sprintf('The "%s" option could accept a string DSN, an array with DSN key, or null. It accept extra options. To find out what option you can set, look at stats storage constructor doc block.', $name)) 43 | ->beforeNormalization() 44 | ->always(function ($v) { 45 | if (\is_array($v)) { 46 | if (isset($v['storage_factory_class'], $v['storage_factory_service'])) { 47 | throw new \LogicException('Both options storage_factory_class and storage_factory_service are set. Please choose one.'); 48 | } 49 | 50 | return $v; 51 | } 52 | 53 | if (is_string($v)) { 54 | return ['dsn' => $v]; 55 | } 56 | 57 | return $v; 58 | }) 59 | ->end() 60 | ->ignoreExtraKeys(false) 61 | ->children() 62 | ->scalarNode('dsn') 63 | ->cannotBeEmpty() 64 | ->isRequired() 65 | ->info(sprintf('The stats storage DSN. These schemes are supported: "%s".', implode('", "', array_keys(Resources::getKnownSchemes())))) 66 | ->end() 67 | ->scalarNode('storage_factory_service') 68 | ->info(sprintf('The factory class should implement "%s" interface', StatsStorageFactory::class)) 69 | ->end() 70 | ->scalarNode('storage_factory_class') 71 | ->info(sprintf('The factory service should be a class that implements "%s" interface', StatsStorageFactory::class)) 72 | ->end() 73 | ->end() 74 | ; 75 | 76 | return $builder; 77 | } 78 | 79 | public function buildStorage(ContainerBuilder $container, array $config): void 80 | { 81 | $storageId = $this->diUtils->format('storage'); 82 | $storageFactoryId = $this->diUtils->format('storage.factory'); 83 | 84 | if (isset($config['storage_factory_service'])) { 85 | $container->setAlias($storageFactoryId, $config['storage_factory_service']); 86 | } elseif (isset($config['storage_factory_class'])) { 87 | $container->register($storageFactoryId, $config['storage_factory_class']); 88 | } else { 89 | $container->register($storageFactoryId, GenericStatsStorageFactory::class); 90 | } 91 | 92 | unset($config['storage_factory_service'], $config['storage_factory_class']); 93 | 94 | $container->register($storageId, StatsStorage::class) 95 | ->setFactory([new Reference($storageFactoryId), 'create']) 96 | ->addArgument($config) 97 | ; 98 | } 99 | 100 | public function buildClientExtension(ContainerBuilder $container, array $config): void 101 | { 102 | $container->register($this->diUtils->format('client_extension'), ClientMonitoringExtension::class) 103 | ->addArgument($this->diUtils->reference('storage')) 104 | ->addArgument(new Reference('logger')) 105 | ->addTag('enqueue.client_extension', ['client' => $this->diUtils->getConfigName()]) 106 | ; 107 | } 108 | 109 | public function buildConsumerExtension(ContainerBuilder $container, array $config): void 110 | { 111 | $container->register($this->diUtils->format('consumer_extension'), ConsumerMonitoringExtension::class) 112 | ->addArgument($this->diUtils->reference('storage')) 113 | ->addTag('enqueue.consumption_extension', ['client' => $this->diUtils->getConfigName()]) 114 | ->addTag('enqueue.transport.consumption_extension', ['transport' => $this->diUtils->getConfigName()]) 115 | ; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /WampStorage.php: -------------------------------------------------------------------------------- 1 | 'wamp://127.0.0.1:9090', 44 | * 'host' => '127.0.0.1', 45 | * 'port' => '9090', 46 | * 'topic' => 'stats', 47 | * 'max_retries' => 15, 48 | * 'initial_retry_delay' => 1.5, 49 | * 'max_retry_delay' => 300, 50 | * 'retry_delay_growth' => 1.5, 51 | * ] 52 | * 53 | * or 54 | * 55 | * wamp://127.0.0.1:9090?max_retries=10 56 | * 57 | * @param array|string|null $config 58 | */ 59 | public function __construct($config = 'wamp:') 60 | { 61 | if (false == class_exists(Client::class) || false == class_exists(PawlTransportProvider::class)) { 62 | throw new \LogicException('Seems client libraries are not installed. Please install "thruway/client" and "thruway/pawl-transport"'); 63 | } 64 | 65 | if (empty($config)) { 66 | $config = $this->parseDsn('wamp:'); 67 | } elseif (is_string($config)) { 68 | $config = $this->parseDsn($config); 69 | } elseif (is_array($config)) { 70 | $config = empty($config['dsn']) ? $config : $this->parseDsn($config['dsn']); 71 | } else { 72 | throw new \LogicException('The config must be either an array of options, a DSN string or null'); 73 | } 74 | 75 | $config = array_replace([ 76 | 'host' => '127.0.0.1', 77 | 'port' => '9090', 78 | 'topic' => 'stats', 79 | 'max_retries' => 15, 80 | 'initial_retry_delay' => 1.5, 81 | 'max_retry_delay' => 300, 82 | 'retry_delay_growth' => 1.5, 83 | ], $config); 84 | 85 | $this->config = $config; 86 | 87 | $this->serialiser = new JsonSerializer(); 88 | } 89 | 90 | public function pushConsumerStats(ConsumerStats $stats): void 91 | { 92 | $this->push($stats); 93 | } 94 | 95 | public function pushConsumedMessageStats(ConsumedMessageStats $stats): void 96 | { 97 | $this->push($stats); 98 | } 99 | 100 | public function pushSentMessageStats(SentMessageStats $stats): void 101 | { 102 | $this->push($stats); 103 | } 104 | 105 | private function push(Stats $stats) 106 | { 107 | $init = false; 108 | $this->stats = $stats; 109 | 110 | if (null === $this->client) { 111 | $init = true; 112 | 113 | $this->client = $this->createClient(); 114 | $this->client->setAttemptRetry(true); 115 | $this->client->on('open', function (ClientSession $session) { 116 | $this->session = $session; 117 | 118 | $this->doSendMessageIfPossible(); 119 | }); 120 | 121 | $this->client->on('close', function () { 122 | if ($this->session === $this->client->getSession()) { 123 | $this->session = null; 124 | } 125 | }); 126 | 127 | $this->client->on('error', function () { 128 | if ($this->session === $this->client->getSession()) { 129 | $this->session = null; 130 | } 131 | }); 132 | 133 | $this->client->on('do-send', function (Stats $stats) { 134 | $onFinish = function () { 135 | $this->client->emit('do-stop'); 136 | }; 137 | 138 | $payload = $this->serialiser->toString($stats); 139 | 140 | $this->session->publish('stats', [$payload], [], ['acknowledge' => true]) 141 | ->then($onFinish, $onFinish); 142 | }); 143 | 144 | $this->client->on('do-stop', function () { 145 | $this->client->getLoop()->stop(); 146 | }); 147 | } 148 | 149 | $this->client->getLoop()->futureTick(function () { 150 | $this->doSendMessageIfPossible(); 151 | }); 152 | 153 | if ($init) { 154 | $this->client->start(false); 155 | } 156 | 157 | $this->client->getLoop()->run(); 158 | } 159 | 160 | private function doSendMessageIfPossible() 161 | { 162 | if (null === $this->session) { 163 | return; 164 | } 165 | 166 | if (null === $this->stats) { 167 | return; 168 | } 169 | 170 | $stats = $this->stats; 171 | 172 | $this->stats = null; 173 | 174 | $this->client->emit('do-send', [$stats]); 175 | } 176 | 177 | private function createClient(): Client 178 | { 179 | $uri = sprintf('ws://%s:%s', $this->config['host'], $this->config['port']); 180 | 181 | $client = new Client('realm1'); 182 | $client->addTransportProvider(new PawlTransportProvider($uri)); 183 | $client->setReconnectOptions([ 184 | 'max_retries' => $this->config['max_retries'], 185 | 'initial_retry_delay' => $this->config['initial_retry_delay'], 186 | 'max_retry_delay' => $this->config['max_retry_delay'], 187 | 'retry_delay_growth' => $this->config['retry_delay_growth'], 188 | ]); 189 | 190 | return $client; 191 | } 192 | 193 | private function parseDsn(string $dsn): array 194 | { 195 | $dsn = Dsn::parseFirst($dsn); 196 | 197 | if (false === in_array($dsn->getSchemeProtocol(), ['wamp', 'ws'], true)) { 198 | throw new \LogicException(sprintf('The given scheme protocol "%s" is not supported. It must be "wamp"', $dsn->getSchemeProtocol())); 199 | } 200 | 201 | return array_filter(array_replace($dsn->getQuery(), [ 202 | 'host' => $dsn->getHost(), 203 | 'port' => $dsn->getPort(), 204 | 'topic' => $dsn->getString('topic'), 205 | 'max_retries' => $dsn->getDecimal('max_retries'), 206 | 'initial_retry_delay' => $dsn->getFloat('initial_retry_delay'), 207 | 'max_retry_delay' => $dsn->getDecimal('max_retry_delay'), 208 | 'retry_delay_growth' => $dsn->getFloat('retry_delay_growth'), 209 | ]), function ($value) { return null !== $value; }); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "enqueue/monitoring", 3 | "type": "library", 4 | "description": "Enqueue Monitoring", 5 | "keywords": ["messaging", "queue", "monitoring", "grafana"], 6 | "homepage": "https://enqueue.forma-pro.com/", 7 | "license": "MIT", 8 | "require": { 9 | "php": "^8.1", 10 | "enqueue/enqueue": "^0.10", 11 | "enqueue/dsn": "^0.10" 12 | }, 13 | "require-dev": { 14 | "phpunit/phpunit": "^9.5", 15 | "enqueue/test": "0.10.x-dev", 16 | "influxdb/influxdb-php": "^1.14", 17 | "datadog/php-datadogstatsd": "^1.3", 18 | "thruway/client": "^0.5.5", 19 | "thruway/pawl-transport": "^0.5", 20 | "voryx/thruway-common": "^1.0.1" 21 | }, 22 | "suggest": { 23 | "thruway/client": "Client for Thruway and the WAMP (Web Application Messaging Protocol).", 24 | "thruway/pawl-transport": "Pawl WebSocket Transport for Thruway Client", 25 | "influxdb/influxdb-php": "A PHP Client for InfluxDB, a time series database", 26 | "datadog/php-datadogstatsd": "Datadog monitoring tool PHP integration" 27 | }, 28 | "support": { 29 | "email": "opensource@forma-pro.com", 30 | "issues": "https://github.com/php-enqueue/enqueue-dev/issues", 31 | "forum": "https://gitter.im/php-enqueue/Lobby", 32 | "source": "https://github.com/php-enqueue/enqueue-dev", 33 | "docs": "https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md" 34 | }, 35 | "autoload": { 36 | "psr-4": { "Enqueue\\Monitoring\\": "" }, 37 | "exclude-from-classmap": [ 38 | "/Tests/" 39 | ] 40 | }, 41 | "minimum-stability": "dev", 42 | "extra": { 43 | "branch-alias": { 44 | "dev-master": "0.10.x-dev" 45 | } 46 | } 47 | } 48 | --------------------------------------------------------------------------------