├── .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 |