Repository: endclothing/prometheus_client_php Branch: master Commit: b0931c73f920 Files: 61 Total size: 147.6 KB Directory structure: gitextract_3xj95wis/ ├── .circleci/ │ └── config.yml ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── docker-compose.yml ├── examples/ │ ├── flush_adapter.php │ ├── metrics.php │ ├── pushgateway.php │ ├── some_counter.php │ ├── some_gauge.php │ └── some_histogram.php ├── nginx/ │ ├── Dockerfile │ ├── default.conf │ └── nginx.conf ├── php-fpm/ │ ├── Dockerfile │ ├── docker-php-ext-apcu-cli.ini │ └── www.conf ├── phpcs.xml.dist ├── phpunit.xml.dist ├── src/ │ └── Prometheus/ │ ├── Collector.php │ ├── CollectorRegistry.php │ ├── Counter.php │ ├── Exception/ │ │ ├── MetricNotFoundException.php │ │ ├── MetricsRegistrationException.php │ │ └── StorageException.php │ ├── Gauge.php │ ├── Histogram.php │ ├── MetricFamilySamples.php │ ├── PushGateway.php │ ├── RenderTextFormat.php │ ├── Sample.php │ └── Storage/ │ ├── APC.php │ ├── Adapter.php │ ├── InMemory.php │ └── Redis.php └── tests/ ├── Test/ │ ├── BlackBoxPushGatewayTest.php │ ├── BlackBoxTest.php │ └── Prometheus/ │ ├── APC/ │ │ ├── CollectorRegistryTest.php │ │ ├── CounterTest.php │ │ ├── GaugeTest.php │ │ └── HistogramTest.php │ ├── AbstractCollectorRegistryTest.php │ ├── AbstractCounterTest.php │ ├── AbstractGaugeTest.php │ ├── AbstractHistogramTest.php │ ├── InMemory/ │ │ ├── CollectorRegistryTest.php │ │ ├── CounterTest.php │ │ ├── GaugeTest.php │ │ └── HistogramTest.php │ ├── PushGatewayTest.php │ ├── Redis/ │ │ ├── CollectorRegistryTest.php │ │ ├── CounterTest.php │ │ ├── CounterWithPrefixTest.php │ │ ├── GaugeTest.php │ │ ├── GaugeWithPrefixTest.php │ │ ├── HistogramTest.php │ │ └── HistogramWithPrefixTest.php │ └── Storage/ │ └── RedisTest.php └── bootstrap.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .circleci/config.yml ================================================ unit-config: &unit-config environment: COMPOSER_PREFER_LOWEST: 0 steps: - checkout - run: name: Install latest composer command: | php -r "copy('https://raw.githubusercontent.com/composer/getcomposer.org/master/web/installer', 'composer-setup.php');" php composer-setup.php php -r "unlink('composer-setup.php');" mv composer.phar /usr/local/bin/composer - run: name: Write COMPOSER_PREFER_LOWEST to file command: echo "$COMPOSER_PREFER_LOWEST" > ~/COMPOSER_PREFER_LOWEST.txt # Download and cache dependencies - restore_cache: keys: - v1-composer-cache-{{ checksum "~/COMPOSER_PREFER_LOWEST.txt" }}-{{ checksum "composer.json" }} # Fall back to using the latest cache if no exact match is found. - v1-composer-cache-{{ checksum "~/COMPOSER_PREFER_LOWEST.txt" }} - run: name: Validate composer files command: composer validate --strict - run: name: Install PHP extensions command: | printf '\n' | sudo pecl install redis sudo docker-php-ext-enable redis - run: name: Install composer packages command: | if [ $COMPOSER_PREFER_LOWEST -eq "1" ]; then composer update --no-ansi --no-progress --optimize-autoloader --no-interaction --no-plugins --no-scripts --prefer-dist --prefer-stable --prefer-lowest else composer update --no-ansi --no-progress --optimize-autoloader --no-interaction --no-plugins --no-scripts --prefer-dist --prefer-stable fi - save_cache: key: v1-composer-cache-{{ checksum "~/COMPOSER_PREFER_LOWEST.txt" }}-{{ checksum "composer.json" }} paths: - ./vendor - run: name: Wait for backing services to start command: dockerize -wait tcp://127.0.0.1:6379 -timeout 30s - run: name: Run PHP Code Sniffer command: ./vendor/bin/phpcs - run: name: Run tests command: ./vendor/bin/phpunit version: 2.1 jobs: php74: <<: *unit-config docker: - image: circleci/php:7.4-cli-node - image: circleci/redis:5-alpine environment: COMPOSER_PREFER_LOWEST: 0 php74-lowest: <<: *unit-config docker: - image: circleci/php:7.4-cli-node - image: circleci/redis:5-alpine environment: COMPOSER_PREFER_LOWEST: 1 php73: <<: *unit-config docker: - image: circleci/php:7.3-cli-node - image: circleci/redis:5-alpine environment: COMPOSER_PREFER_LOWEST: 0 php73-lowest: <<: *unit-config docker: - image: circleci/php:7.3-cli-node - image: circleci/redis:5-alpine environment: COMPOSER_PREFER_LOWEST: 1 php72: <<: *unit-config docker: - image: circleci/php:7.2-cli-node - image: circleci/redis:5-alpine environment: COMPOSER_PREFER_LOWEST: 0 php72-lowest: <<: *unit-config docker: - image: circleci/php:7.2-cli-node - image: circleci/redis:5-alpine environment: COMPOSER_PREFER_LOWEST: 1 workflows: version: 2 units: jobs: - php74 - php74-lowest - php73 - php73-lowest - php72 - php72-lowest ================================================ FILE: .gitattributes ================================================ * text=auto /.circleci/ export-ignore /.github/ export-ignore /examples/ export-ignore /nginx/ export-ignore /php-fpm/ export-ignore /tests/ export-ignore .gitattributes export-ignore .gitignore export-ignore docker-compose.yml export-ignore phpcs.xml.dist export-ignore phpunit.xml.dist export-ignore ================================================ FILE: .gitignore ================================================ /vendor/ *.iml /.idea/ composer.lock composer.phar .phpunit.result.cache ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2016 Jimdo GmbH Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # A prometheus client library written in PHP [![CircleCI](https://circleci.com/gh/endclothing/prometheus_client_php/tree/master.svg?style=shield)](https://circleci.com/gh/endclothing/prometheus_client_php/tree/master) This library uses Redis or APCu to do the client side aggregation. If using Redis, we recommend to run a local Redis instance next to your PHP workers. ## How does it work? Usually PHP worker processes don't share any state. You can pick from three adapters. Redis, APC or an in memory adapter. While the first needs a separate binary running, the second just needs the [APC](https://pecl.php.net/package/APCU) extension to be installed. If you don't need persistent metrics between requests (e.g. a long running cron job or script) the in memory adapter might be suitable to use. ## Installation Add as [Composer](https://getcomposer.org/) dependency: ```sh composer require endclothing/prometheus_client_php ``` ## Usage A simple counter: ```php \Prometheus\CollectorRegistry::getDefault() ->getOrRegisterCounter('', 'some_quick_counter', 'just a quick measurement') ->inc(); ``` Write some enhanced metrics: ```php $registry = \Prometheus\CollectorRegistry::getDefault(); $counter = $registry->getOrRegisterCounter('test', 'some_counter', 'it increases', ['type']); $counter->incBy(3, ['blue']); $gauge = $registry->getOrRegisterGauge('test', 'some_gauge', 'it sets', ['type']); $gauge->set(2.5, ['blue']); $histogram = $registry->getOrRegisterHistogram('test', 'some_histogram', 'it observes', ['type'], [0.1, 1, 2, 3.5, 4, 5, 6, 7, 8, 9]); $histogram->observe(3.5, ['blue']); ``` Manually register and retrieve metrics (these steps are combined in the `getOrRegister...` methods): ```php $registry = \Prometheus\CollectorRegistry::getDefault(); $counterA = $registry->registerCounter('test', 'some_counter', 'it increases', ['type']); $counterA->incBy(3, ['blue']); // once a metric is registered, it can be retrieved using e.g. getCounter: $counterB = $registry->getCounter('test', 'some_counter') $counterB->incBy(2, ['red']); ``` Expose the metrics: ```php $registry = \Prometheus\CollectorRegistry::getDefault(); $renderer = new RenderTextFormat(); $result = $renderer->render($registry->getMetricFamilySamples()); header('Content-type: ' . RenderTextFormat::MIME_TYPE); echo $result; ``` Change the Redis options (the example shows the defaults): ```php \Prometheus\Storage\Redis::setDefaultOptions( [ 'host' => '127.0.0.1', 'port' => 6379, 'password' => null, 'timeout' => 0.1, // in seconds 'read_timeout' => '10', // in seconds 'persistent_connections' => false ] ); ``` Using the InMemory storage: ```php $registry = new CollectorRegistry(new InMemory()); $counter = $registry->registerCounter('test', 'some_counter', 'it increases', ['type']); $counter->incBy(3, ['blue']); $renderer = new RenderTextFormat(); $result = $renderer->render($registry->getMetricFamilySamples()); ``` ### Advanced Usage #### Advanced Histogram Usage On passing an empty array for the bucket parameter on instantiation, a set of default buckets will be used instead. Whilst this is a good base for a typical web application, there is named constructor to assist in the generation of exponential / geometric buckets. Eg: ``` Histogram::exponentialBuckets(0.05, 1.5, 10); ``` This will start your buckets with a value of 1.5, grow them by a factor of 1.5 per bucket across a set of 10 buckets. Also look at the [examples](examples). ## Development ### Dependencies * PHP ^7.3 * PHP Redis extension * PHP APCu extension * [Composer](https://getcomposer.org/doc/00-intro.md#installation-linux-unix-osx) * Redis Start a Redis instance: ``` docker-compose up Redis ``` Run the tests: ``` composer install # when Redis is not listening on localhost: # export REDIS_HOST=192.168.59.100 ./vendor/bin/phpunit ``` ## Black box testing Just start the nginx, fpm & Redis setup with docker-compose: ``` docker-compose up ``` Pick the adapter you want to test. ``` docker-compose run phpunit env ADAPTER=apc vendor/bin/phpunit tests/Test/ docker-compose run phpunit env ADAPTER=redis vendor/bin/phpunit tests/Test/ ``` ================================================ FILE: composer.json ================================================ { "name": "endclothing/prometheus_client_php", "description": "Prometheus instrumentation library for PHP applications.", "type": "library", "license": "Apache-2.0", "authors": [ { "name": "Daniel Noel-Davies", "email": "Daniel.Noel-Davies@endclothing.com" } ], "replace": { "jimdo/prometheus_client_php": "*" }, "require": { "php": "^7.2", "ext-json": "*", "guzzlehttp/guzzle": "^6.3", "symfony/polyfill-apcu": "^1.6" }, "require-dev": { "phpunit/phpunit": "^8.4", "squizlabs/php_codesniffer": "^3.5" }, "suggest": { "ext-redis": "Required if using Redis.", "ext-apc": "Required if using APCu." }, "autoload": { "psr-4": { "Prometheus\\": "src/Prometheus/" } }, "autoload-dev": { "psr-0": { "Test\\Prometheus\\": "tests/" } }, "extra": { "branch-alias": { "dev-master": "1.0-dev" } }, "config": { "sort-packages": true }, "prefer-stable": true } ================================================ FILE: docker-compose.yml ================================================ nginx: build: nginx/ links: - php-fpm ports: - 8080:80 php-fpm: build: php-fpm/ volumes: - .:/var/www/html links: - redis - pushgateway environment: - REDIS_HOST=redis redis: image: redis ports: - 6379:6379 pushgateway: image: prom/pushgateway ports: - 9091:9091 phpunit: build: php-fpm/ volumes: - .:/var/www/html links: - redis - pushgateway - nginx environment: - REDIS_HOST=redis ================================================ FILE: examples/flush_adapter.php ================================================ REDIS_HOST]); $redisAdapter->flushRedis(); } elseif ($adapter === 'apc') { $apcAdapter = new Prometheus\Storage\APC(); $apcAdapter->flushAPC(); } elseif ($adapter === 'in-memory') { $inMemoryAdapter = new Prometheus\Storage\InMemory(); $inMemoryAdapter->flushMemory(); } ================================================ FILE: examples/metrics.php ================================================ $_SERVER['REDIS_HOST'] ?? '127.0.0.1']); $adapter = new Prometheus\Storage\Redis(); } elseif ($adapter === 'apc') { $adapter = new Prometheus\Storage\APC(); } elseif ($adapter === 'in-memory') { $adapter = new Prometheus\Storage\InMemory(); } $registry = new CollectorRegistry($adapter); $renderer = new RenderTextFormat(); $result = $renderer->render($registry->getMetricFamilySamples()); header('Content-type: ' . RenderTextFormat::MIME_TYPE); echo $result; ================================================ FILE: examples/pushgateway.php ================================================ $_SERVER['REDIS_HOST'] ?? '127.0.0.1']); $adapter = new Prometheus\Storage\Redis(); } elseif ($adapter === 'apc') { $adapter = new Prometheus\Storage\APC(); } elseif ($adapter === 'in-memory') { $adapter = new Prometheus\Storage\InMemory(); } $registry = new CollectorRegistry($adapter); $counter = $registry->registerCounter('test', 'some_counter', 'it increases', ['type']); $counter->incBy(6, ['blue']); $pushGateway = new PushGateway('192.168.59.100:9091'); $pushGateway->push($registry, 'my_job', ['instance' => 'foo']); ================================================ FILE: examples/some_counter.php ================================================ $_SERVER['REDIS_HOST'] ?? '127.0.0.1']); $adapter = new Prometheus\Storage\Redis(); } elseif ($adapter === 'apc') { $adapter = new Prometheus\Storage\APC(); } elseif ($adapter === 'in-memory') { $adapter = new Prometheus\Storage\InMemory(); } $registry = new CollectorRegistry($adapter); $counter = $registry->registerCounter('test', 'some_counter', 'it increases', ['type']); $counter->incBy($_GET['c'], ['blue']); echo "OK\n"; ================================================ FILE: examples/some_gauge.php ================================================ $_SERVER['REDIS_HOST'] ?? '127.0.0.1']); $adapter = new Prometheus\Storage\Redis(); } elseif ($adapter === 'apc') { $adapter = new Prometheus\Storage\APC(); } elseif ($adapter === 'in-memory') { $adapter = new Prometheus\Storage\InMemory(); } $registry = new CollectorRegistry($adapter); $gauge = $registry->registerGauge('test', 'some_gauge', 'it sets', ['type']); $gauge->set($_GET['c'], ['blue']); echo "OK\n"; ================================================ FILE: examples/some_histogram.php ================================================ $_SERVER['REDIS_HOST'] ?? '127.0.0.1']); $adapter = new Prometheus\Storage\Redis(); } elseif ($adapter === 'apc') { $adapter = new Prometheus\Storage\APC(); } elseif ($adapter === 'in-memory') { $adapter = new Prometheus\Storage\InMemory(); } $registry = new CollectorRegistry($adapter); $histogram = $registry->registerHistogram('test', 'some_histogram', 'it observes', ['type'], [0.1, 1, 2, 3.5, 4, 5, 6, 7, 8, 9]); $histogram->observe($_GET['c'], ['blue']); echo "OK\n"; ================================================ FILE: nginx/Dockerfile ================================================ FROM nginx RUN rm /etc/nginx/conf.d/default.conf /etc/nginx/nginx.conf COPY nginx.conf /etc/nginx/ COPY default.conf /etc/nginx/conf.d/ ================================================ FILE: nginx/default.conf ================================================ server { listen 80; root /var/www/html; error_log /var/log/nginx/localhost.error.log; access_log /var/log/nginx/localhost.access.log; location ~ ^/.+\.php(/|$) { fastcgi_pass php-fpm:9000; fastcgi_split_path_info ^(.+\.php)(/.*)$; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param HTTPS off; } } ================================================ FILE: nginx/nginx.conf ================================================ user nginx; worker_processes 1; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 4096; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; #tcp_nopush on; keepalive_timeout 65; #gzip on; include /etc/nginx/conf.d/*.conf; } ================================================ FILE: php-fpm/Dockerfile ================================================ FROM php:5.6-fpm RUN pecl install redis-2.2.8 && docker-php-ext-enable redis RUN pecl install apcu-4.0.11 && docker-php-ext-enable apcu COPY www.conf /usr/local/etc/php-fpm.d/ COPY docker-php-ext-apcu-cli.ini /usr/local/etc/php/conf.d/ ================================================ FILE: php-fpm/docker-php-ext-apcu-cli.ini ================================================ apc.enable_cli = On ================================================ FILE: php-fpm/www.conf ================================================ [www] user = www-data group = www-data listen = 127.0.0.1:9000 pm = static pm.max_children = 20 ================================================ FILE: phpcs.xml.dist ================================================ src/ tests/ examples/ vendor/* ================================================ FILE: phpunit.xml.dist ================================================ ./tests/Test/Prometheus ./src/Prometheus ================================================ FILE: src/Prometheus/Collector.php ================================================ storageAdapter = $storageAdapter; $metricName = ($namespace ? $namespace . '_' : '') . $name; if (!preg_match(self::RE_METRIC_LABEL_NAME, $metricName)) { throw new InvalidArgumentException("Invalid metric name: '" . $metricName . "'"); } $this->name = $metricName; $this->help = $help; foreach ($labels as $label) { if (!preg_match(self::RE_METRIC_LABEL_NAME, $label)) { throw new InvalidArgumentException("Invalid label name: '" . $label . "'"); } } $this->labels = $labels; } /** * @return string */ abstract public function getType(); /** * @return string */ public function getName(): string { return $this->name; } /** * @return array */ public function getLabelNames(): array { return $this->labels; } /** * @return string */ public function getHelp(): string { return $this->help; } /** * @return string */ public function getKey(): string { return sha1($this->getName() . serialize($this->getLabelNames())); } /** * @param $labels */ protected function assertLabelsAreDefinedCorrectly($labels): void { if (count($labels) != count($this->labels)) { throw new InvalidArgumentException(sprintf('Labels are not defined correctly: %s', print_r($labels, true))); } } } ================================================ FILE: src/Prometheus/CollectorRegistry.php ================================================ storageAdapter = $redisAdapter; } /** * @return CollectorRegistry */ public static function getDefault(): CollectorRegistry { if (!self::$defaultRegistry) { return self::$defaultRegistry = new static(new Redis()); } return self::$defaultRegistry; } /** * @return MetricFamilySamples[] */ public function getMetricFamilySamples(): array { return $this->storageAdapter->collect(); } /** * @param string $namespace e.g. cms * @param string $name e.g. duration_seconds * @param string $help e.g. The duration something took in seconds. * @param array $labels e.g. ['controller', 'action'] * @return Gauge * @throws MetricsRegistrationException */ public function registerGauge($namespace, $name, $help, $labels = []): Gauge { $metricIdentifier = self::metricIdentifier($namespace, $name); if (isset($this->gauges[$metricIdentifier])) { throw new MetricsRegistrationException("Metric already registered"); } $this->gauges[$metricIdentifier] = new Gauge( $this->storageAdapter, $namespace, $name, $help, $labels ); return $this->gauges[$metricIdentifier]; } /** * @param string $namespace * @param string $name * @return Gauge * @throws MetricNotFoundException */ public function getGauge($namespace, $name): Gauge { $metricIdentifier = self::metricIdentifier($namespace, $name); if (!isset($this->gauges[$metricIdentifier])) { throw new MetricNotFoundException("Metric not found:" . $metricIdentifier); } return $this->gauges[$metricIdentifier]; } /** * @param string $namespace e.g. cms * @param string $name e.g. duration_seconds * @param string $help e.g. The duration something took in seconds. * @param array $labels e.g. ['controller', 'action'] * @return Gauge * @throws MetricsRegistrationException */ public function getOrRegisterGauge($namespace, $name, $help, $labels = []): Gauge { try { $gauge = $this->getGauge($namespace, $name); } catch (MetricNotFoundException $e) { $gauge = $this->registerGauge($namespace, $name, $help, $labels); } return $gauge; } /** * @param string $namespace e.g. cms * @param string $name e.g. requests * @param string $help e.g. The number of requests made. * @param array $labels e.g. ['controller', 'action'] * @return Counter * @throws MetricsRegistrationException */ public function registerCounter($namespace, $name, $help, $labels = []): Counter { $metricIdentifier = self::metricIdentifier($namespace, $name); if (isset($this->counters[$metricIdentifier])) { throw new MetricsRegistrationException("Metric already registered"); } $this->counters[$metricIdentifier] = new Counter( $this->storageAdapter, $namespace, $name, $help, $labels ); return $this->counters[self::metricIdentifier($namespace, $name)]; } /** * @param string $namespace * @param string $name * @return Counter * @throws MetricNotFoundException */ public function getCounter($namespace, $name): Counter { $metricIdentifier = self::metricIdentifier($namespace, $name); if (!isset($this->counters[$metricIdentifier])) { throw new MetricNotFoundException("Metric not found:" . $metricIdentifier); } return $this->counters[self::metricIdentifier($namespace, $name)]; } /** * @param string $namespace e.g. cms * @param string $name e.g. requests * @param string $help e.g. The number of requests made. * @param array $labels e.g. ['controller', 'action'] * @return Counter * @throws MetricsRegistrationException */ public function getOrRegisterCounter($namespace, $name, $help, $labels = []): Counter { try { $counter = $this->getCounter($namespace, $name); } catch (MetricNotFoundException $e) { $counter = $this->registerCounter($namespace, $name, $help, $labels); } return $counter; } /** * @param string $namespace e.g. cms * @param string $name e.g. duration_seconds * @param string $help e.g. A histogram of the duration in seconds. * @param array $labels e.g. ['controller', 'action'] * @param array $buckets e.g. [100, 200, 300] * @return Histogram * @throws MetricsRegistrationException */ public function registerHistogram($namespace, $name, $help, $labels = [], $buckets = null): Histogram { $metricIdentifier = self::metricIdentifier($namespace, $name); if (isset($this->histograms[$metricIdentifier])) { throw new MetricsRegistrationException("Metric already registered"); } $this->histograms[$metricIdentifier] = new Histogram( $this->storageAdapter, $namespace, $name, $help, $labels, $buckets ); return $this->histograms[$metricIdentifier]; } /** * @param string $namespace * @param string $name * @return Histogram * @throws MetricNotFoundException */ public function getHistogram($namespace, $name): Histogram { $metricIdentifier = self::metricIdentifier($namespace, $name); if (!isset($this->histograms[$metricIdentifier])) { throw new MetricNotFoundException("Metric not found:" . $metricIdentifier); } return $this->histograms[self::metricIdentifier($namespace, $name)]; } /** * @param string $namespace e.g. cms * @param string $name e.g. duration_seconds * @param string $help e.g. A histogram of the duration in seconds. * @param array $labels e.g. ['controller', 'action'] * @param array $buckets e.g. [100, 200, 300] * @return Histogram * @throws MetricsRegistrationException */ public function getOrRegisterHistogram($namespace, $name, $help, $labels = [], $buckets = null): Histogram { try { $histogram = $this->getHistogram($namespace, $name); } catch (MetricNotFoundException $e) { $histogram = $this->registerHistogram($namespace, $name, $help, $labels, $buckets); } return $histogram; } /** * @param $namespace * @param $name * @return string */ private static function metricIdentifier($namespace, $name): string { return $namespace . ":" . $name; } } ================================================ FILE: src/Prometheus/Counter.php ================================================ incBy(1, $labels); } /** * @param int $count e.g. 2 * @param array $labels e.g. ['status', 'opcode'] */ public function incBy($count, array $labels = []): void { $this->assertLabelsAreDefinedCorrectly($labels); $this->storageAdapter->updateCounter( [ 'name' => $this->getName(), 'help' => $this->getHelp(), 'type' => $this->getType(), 'labelNames' => $this->getLabelNames(), 'labelValues' => $labels, 'value' => $count, 'command' => Adapter::COMMAND_INCREMENT_INTEGER, ] ); } } ================================================ FILE: src/Prometheus/Exception/MetricNotFoundException.php ================================================ assertLabelsAreDefinedCorrectly($labels); $this->storageAdapter->updateGauge( [ 'name' => $this->getName(), 'help' => $this->getHelp(), 'type' => $this->getType(), 'labelNames' => $this->getLabelNames(), 'labelValues' => $labels, 'value' => $value, 'command' => Adapter::COMMAND_SET, ] ); } /** * @return string */ public function getType(): string { return self::TYPE; } /** * @param array $labels */ public function inc($labels = []): void { $this->incBy(1, $labels); } /** * @param $value * @param array $labels */ public function incBy($value, array $labels = []): void { $this->assertLabelsAreDefinedCorrectly($labels); $this->storageAdapter->updateGauge( [ 'name' => $this->getName(), 'help' => $this->getHelp(), 'type' => $this->getType(), 'labelNames' => $this->getLabelNames(), 'labelValues' => $labels, 'value' => $value, 'command' => Adapter::COMMAND_INCREMENT_FLOAT, ] ); } /** * @param array $labels */ public function dec($labels = []): void { $this->decBy(1, $labels); } /** * @param $value * @param array $labels */ public function decBy($value, $labels = []): void { $this->incBy(-$value, $labels); } } ================================================ FILE: src/Prometheus/Histogram.php ================================================ = $buckets[$i + 1]) { throw new InvalidArgumentException( "Histogram buckets must be in increasing order: " . $buckets[$i] . " >= " . $buckets[$i + 1] ); } } if (in_array('le', $labels, true)) { throw new \InvalidArgumentException("Histogram cannot have a label named 'le'."); } $this->buckets = $buckets; } /** * List of default buckets suitable for typical web application latency metrics * @return array */ public static function getDefaultBuckets(): array { return [ 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0, ]; } /** * @param float $start * @param float $growthFactor * @param int $numberOfBuckets * @return array */ public static function exponentialBuckets(float $start, float $growthFactor, int $numberOfBuckets): array { if ($numberOfBuckets < 1) { throw new InvalidArgumentException('Number of buckets must be a positive integer'); } if ($start <= 0) { throw new InvalidArgumentException('The starting position of a set of buckets must be a positive integer'); } if ($growthFactor <= 1) { throw new InvalidArgumentException('The growth factor must greater than 1'); } $buckets = []; for ($i = 0; $i < $numberOfBuckets; $i++) { $buckets[$i] = $start; $start *= $growthFactor; } return $buckets; } /** * @param double $value e.g. 123 * @param array $labels e.g. ['status', 'opcode'] */ public function observe(float $value, array $labels = []): void { $this->assertLabelsAreDefinedCorrectly($labels); $this->storageAdapter->updateHistogram( [ 'value' => $value, 'name' => $this->getName(), 'help' => $this->getHelp(), 'type' => $this->getType(), 'labelNames' => $this->getLabelNames(), 'labelValues' => $labels, 'buckets' => $this->buckets, ] ); } /** * @return string */ public function getType(): string { return self::TYPE; } } ================================================ FILE: src/Prometheus/MetricFamilySamples.php ================================================ name = $data['name']; $this->type = $data['type']; $this->help = $data['help']; $this->labelNames = $data['labelNames']; foreach ($data['samples'] as $sampleData) { $this->samples[] = new Sample($sampleData); } } /** * @return string */ public function getName(): string { return $this->name; } /** * @return string */ public function getType(): string { return $this->type; } /** * @return string */ public function getHelp(): string { return $this->help; } /** * @return Sample[] */ public function getSamples(): array { return $this->samples; } /** * @return array */ public function getLabelNames(): array { return $this->labelNames; } /** * @return bool */ public function hasLabelNames(): bool { return !empty($this->labelNames); } } ================================================ FILE: src/Prometheus/PushGateway.php ================================================ address = $address; $this->client = $client ?? new Client(); } /** * Pushes all metrics in a Collector, replacing all those with the same job. * Uses HTTP PUT. * @param CollectorRegistry $collectorRegistry * @param string $job * @param array $groupingKey * @throws GuzzleException */ public function push(CollectorRegistry $collectorRegistry, string $job, array $groupingKey = []): void { $this->doRequest($collectorRegistry, $job, $groupingKey, 'put'); } /** * Pushes all metrics in a Collector, replacing only previously pushed metrics of the same name and job. * Uses HTTP POST. * @param CollectorRegistry $collectorRegistry * @param $job * @param $groupingKey * @throws GuzzleException */ public function pushAdd(CollectorRegistry $collectorRegistry, string $job, array $groupingKey = []): void { $this->doRequest($collectorRegistry, $job, $groupingKey, 'post'); } /** * Deletes metrics from the Push Gateway. * Uses HTTP POST. * @param string $job * @param array $groupingKey * @throws GuzzleException */ public function delete(string $job, array $groupingKey = []): void { $this->doRequest(null, $job, $groupingKey, 'delete'); } /** * @param CollectorRegistry $collectorRegistry * @param string $job * @param array $groupingKey * @param string $method * @throws GuzzleException */ private function doRequest(CollectorRegistry $collectorRegistry, string $job, array $groupingKey, $method): void { $url = "http://" . $this->address . "/metrics/job/" . $job; if (!empty($groupingKey)) { foreach ($groupingKey as $label => $value) { $url .= "/" . $label . "/" . $value; } } $requestOptions = [ 'headers' => [ 'Content-Type' => RenderTextFormat::MIME_TYPE, ], 'connect_timeout' => 10, 'timeout' => 20, ]; if ($method != 'delete') { $renderer = new RenderTextFormat(); $requestOptions['body'] = $renderer->render($collectorRegistry->getMetricFamilySamples()); } $response = $this->client->request($method, $url, $requestOptions); $statusCode = $response->getStatusCode(); if (!in_array($statusCode, [200, 202])) { $msg = "Unexpected status code " . $statusCode . " received from push gateway " . $this->address . ": " . $response->getBody(); throw new RuntimeException($msg); } } } ================================================ FILE: src/Prometheus/RenderTextFormat.php ================================================ getName(), $b->getName()); }); $lines = []; foreach ($metrics as $metric) { $lines[] = "# HELP " . $metric->getName() . " {$metric->getHelp()}"; $lines[] = "# TYPE " . $metric->getName() . " {$metric->getType()}"; foreach ($metric->getSamples() as $sample) { $lines[] = $this->renderSample($metric, $sample); } } return implode("\n", $lines) . "\n"; } /** * @param MetricFamilySamples $metric * @param Sample $sample * @return string */ private function renderSample(MetricFamilySamples $metric, Sample $sample): string { $escapedLabels = []; $labelNames = $metric->getLabelNames(); if ($metric->hasLabelNames() || $sample->hasLabelNames()) { $labels = array_combine(array_merge($labelNames, $sample->getLabelNames()), $sample->getLabelValues()); foreach ($labels as $labelName => $labelValue) { $escapedLabels[] = $labelName . '="' . $this->escapeLabelValue($labelValue) . '"'; } return $sample->getName() . '{' . implode(',', $escapedLabels) . '} ' . $sample->getValue(); } return $sample->getName() . ' ' . $sample->getValue(); } /** * @param string $v * @return string */ private function escapeLabelValue($v): string { $v = str_replace("\\", "\\\\", $v); $v = str_replace("\n", "\\n", $v); $v = str_replace("\"", "\\\"", $v); return $v; } } ================================================ FILE: src/Prometheus/Sample.php ================================================ name = $data['name']; $this->labelNames = $data['labelNames']; $this->labelValues = $data['labelValues']; $this->value = $data['value']; } /** * @return string */ public function getName(): string { return $this->name; } /** * @return array */ public function getLabelNames(): array { return (array)$this->labelNames; } /** * @return array */ public function getLabelValues(): array { return (array)$this->labelValues; } /** * @return int|double */ public function getValue(): string { return (string) $this->value; } /** * @return bool */ public function hasLabelNames(): bool { return !empty($this->labelNames); } } ================================================ FILE: src/Prometheus/Storage/APC.php ================================================ collectHistograms(); $metrics = array_merge($metrics, $this->collectGauges()); $metrics = array_merge($metrics, $this->collectCounters()); return $metrics; } /** * @param array $data */ public function updateHistogram(array $data): void { // Initialize the sum $sumKey = $this->histogramBucketValueKey($data, 'sum'); $new = apcu_add($sumKey, $this->toInteger(0)); // If sum does not exist, assume a new histogram and store the metadata if ($new) { apcu_store($this->metaKey($data), json_encode($this->metaData($data))); } // Atomically increment the sum // Taken from https://github.com/prometheus/client_golang/blob/66058aac3a83021948e5fb12f1f408ff556b9037/prometheus/value.go#L91 $done = false; while (!$done) { $old = apcu_fetch($sumKey); $done = apcu_cas($sumKey, $old, $this->toInteger($this->fromInteger($old) + $data['value'])); } // Figure out in which bucket the observation belongs $bucketToIncrease = '+Inf'; foreach ($data['buckets'] as $bucket) { if ($data['value'] <= $bucket) { $bucketToIncrease = $bucket; break; } } // Initialize and increment the bucket apcu_add($this->histogramBucketValueKey($data, $bucketToIncrease), 0); apcu_inc($this->histogramBucketValueKey($data, $bucketToIncrease)); } /** * @param array $data */ public function updateGauge(array $data): void { $valueKey = $this->valueKey($data); if ($data['command'] == Adapter::COMMAND_SET) { apcu_store($valueKey, $this->toInteger($data['value'])); apcu_store($this->metaKey($data), json_encode($this->metaData($data))); } else { $new = apcu_add($valueKey, $this->toInteger(0)); if ($new) { apcu_store($this->metaKey($data), json_encode($this->metaData($data))); } // Taken from https://github.com/prometheus/client_golang/blob/66058aac3a83021948e5fb12f1f408ff556b9037/prometheus/value.go#L91 $done = false; while (!$done) { $old = apcu_fetch($valueKey); $done = apcu_cas($valueKey, $old, $this->toInteger($this->fromInteger($old) + $data['value'])); } } } /** * @param array $data */ public function updateCounter(array $data): void { $new = apcu_add($this->valueKey($data), 0); if ($new) { apcu_store($this->metaKey($data), json_encode($this->metaData($data))); } apcu_inc($this->valueKey($data), $data['value']); } /** * @return void */ public function flushAPC(): void { apcu_clear_cache(); } /** * @param array $data * @return string */ private function metaKey(array $data): string { return implode(':', [self::PROMETHEUS_PREFIX, $data['type'], $data['name'], 'meta']); } /** * @param array $data * @return string */ private function valueKey(array $data): string { return implode(':', [ self::PROMETHEUS_PREFIX, $data['type'], $data['name'], $this->encodeLabelValues($data['labelValues']), 'value', ]); } /** * @param array $data * @return string */ private function histogramBucketValueKey(array $data, $bucket): string { return implode(':', [ self::PROMETHEUS_PREFIX, $data['type'], $data['name'], $this->encodeLabelValues($data['labelValues']), $bucket, 'value', ]); } /** * @param array $data * @return array */ private function metaData(array $data): array { $metricsMetaData = $data; unset($metricsMetaData['value']); unset($metricsMetaData['command']); unset($metricsMetaData['labelValues']); return $metricsMetaData; } /** * @return array */ private function collectCounters(): array { $counters = []; foreach (new APCUIterator('/^prom:counter:.*:meta/') as $counter) { $metaData = json_decode($counter['value'], true); $data = [ 'name' => $metaData['name'], 'help' => $metaData['help'], 'type' => $metaData['type'], 'labelNames' => $metaData['labelNames'], ]; foreach (new APCUIterator('/^prom:counter:' . $metaData['name'] . ':.*:value/') as $value) { $parts = explode(':', $value['key']); $labelValues = $parts[3]; $data['samples'][] = [ 'name' => $metaData['name'], 'labelNames' => [], 'labelValues' => $this->decodeLabelValues($labelValues), 'value' => $value['value'], ]; } $this->sortSamples($data['samples']); $counters[] = new MetricFamilySamples($data); } return $counters; } /** * @return array */ private function collectGauges(): array { $gauges = []; foreach (new APCUIterator('/^prom:gauge:.*:meta/') as $gauge) { $metaData = json_decode($gauge['value'], true); $data = [ 'name' => $metaData['name'], 'help' => $metaData['help'], 'type' => $metaData['type'], 'labelNames' => $metaData['labelNames'], ]; foreach (new APCUIterator('/^prom:gauge:' . $metaData['name'] . ':.*:value/') as $value) { $parts = explode(':', $value['key']); $labelValues = $parts[3]; $data['samples'][] = [ 'name' => $metaData['name'], 'labelNames' => [], 'labelValues' => $this->decodeLabelValues($labelValues), 'value' => $this->fromInteger($value['value']), ]; } $this->sortSamples($data['samples']); $gauges[] = new MetricFamilySamples($data); } return $gauges; } /** * @return array */ private function collectHistograms(): array { $histograms = []; foreach (new APCUIterator('/^prom:histogram:.*:meta/') as $histogram) { $metaData = json_decode($histogram['value'], true); $data = [ 'name' => $metaData['name'], 'help' => $metaData['help'], 'type' => $metaData['type'], 'labelNames' => $metaData['labelNames'], 'buckets' => $metaData['buckets'], ]; // Add the Inf bucket so we can compute it later on $data['buckets'][] = '+Inf'; $histogramBuckets = []; foreach (new APCUIterator('/^prom:histogram:' . $metaData['name'] . ':.*:value/') as $value) { $parts = explode(':', $value['key']); $labelValues = $parts[3]; $bucket = $parts[4]; // Key by labelValues $histogramBuckets[$labelValues][$bucket] = $value['value']; } // Compute all buckets $labels = array_keys($histogramBuckets); sort($labels); foreach ($labels as $labelValues) { $acc = 0; $decodedLabelValues = $this->decodeLabelValues($labelValues); foreach ($data['buckets'] as $bucket) { $bucket = (string)$bucket; if (!isset($histogramBuckets[$labelValues][$bucket])) { $data['samples'][] = [ 'name' => $metaData['name'] . '_bucket', 'labelNames' => ['le'], 'labelValues' => array_merge($decodedLabelValues, [$bucket]), 'value' => $acc, ]; } else { $acc += $histogramBuckets[$labelValues][$bucket]; $data['samples'][] = [ 'name' => $metaData['name'] . '_' . 'bucket', 'labelNames' => ['le'], 'labelValues' => array_merge($decodedLabelValues, [$bucket]), 'value' => $acc, ]; } } // Add the count $data['samples'][] = [ 'name' => $metaData['name'] . '_count', 'labelNames' => [], 'labelValues' => $decodedLabelValues, 'value' => $acc, ]; // Add the sum $data['samples'][] = [ 'name' => $metaData['name'] . '_sum', 'labelNames' => [], 'labelValues' => $decodedLabelValues, 'value' => $this->fromInteger($histogramBuckets[$labelValues]['sum']), ]; } $histograms[] = new MetricFamilySamples($data); } return $histograms; } /** * @param mixed $val * @return int */ private function toInteger($val): int { return unpack('Q', pack('d', $val))[1]; } /** * @param mixed $val * @return float */ private function fromInteger($val): float { return unpack('d', pack('Q', $val))[1]; } /** * @param array $samples */ private function sortSamples(array &$samples): void { usort($samples, function ($a, $b) { return strcmp(implode("", $a['labelValues']), implode("", $b['labelValues'])); }); } /** * @param array $values * @return string * @throws RuntimeException */ private function encodeLabelValues(array $values): string { $json = json_encode($values); if (false === $json) { throw new RuntimeException(json_last_error_msg()); } return base64_encode($json); } /** * @param string $values * @return array * @throws RuntimeException */ private function decodeLabelValues($values): array { $json = base64_decode($values, true); if (false === $json) { throw new RuntimeException('Cannot base64 decode label values'); } $decodedValues = json_decode($json, true); if (false === $decodedValues) { throw new RuntimeException(json_last_error_msg()); } return $decodedValues; } } ================================================ FILE: src/Prometheus/Storage/Adapter.php ================================================ internalCollect($this->counters); $metrics = array_merge($metrics, $this->internalCollect($this->gauges)); $metrics = array_merge($metrics, $this->collectHistograms()); return $metrics; } public function flushMemory(): void { $this->counters = []; $this->gauges = []; $this->histograms = []; } /** * @return array */ private function collectHistograms(): array { $histograms = []; foreach ($this->histograms as $histogram) { $metaData = $histogram['meta']; $data = [ 'name' => $metaData['name'], 'help' => $metaData['help'], 'type' => $metaData['type'], 'labelNames' => $metaData['labelNames'], 'buckets' => $metaData['buckets'], ]; // Add the Inf bucket so we can compute it later on $data['buckets'][] = '+Inf'; $histogramBuckets = []; foreach ($histogram['samples'] as $key => $value) { $parts = explode(':', $key); $labelValues = $parts[2]; $bucket = $parts[3]; // Key by labelValues $histogramBuckets[$labelValues][$bucket] = $value; } // Compute all buckets $labels = array_keys($histogramBuckets); sort($labels); foreach ($labels as $labelValues) { $acc = 0; $decodedLabelValues = $this->decodeLabelValues($labelValues); foreach ($data['buckets'] as $bucket) { $bucket = (string)$bucket; if (!isset($histogramBuckets[$labelValues][$bucket])) { $data['samples'][] = [ 'name' => $metaData['name'] . '_bucket', 'labelNames' => ['le'], 'labelValues' => array_merge($decodedLabelValues, [$bucket]), 'value' => $acc, ]; } else { $acc += $histogramBuckets[$labelValues][$bucket]; $data['samples'][] = [ 'name' => $metaData['name'] . '_' . 'bucket', 'labelNames' => ['le'], 'labelValues' => array_merge($decodedLabelValues, [$bucket]), 'value' => $acc, ]; } } // Add the count $data['samples'][] = [ 'name' => $metaData['name'] . '_count', 'labelNames' => [], 'labelValues' => $decodedLabelValues, 'value' => $acc, ]; // Add the sum $data['samples'][] = [ 'name' => $metaData['name'] . '_sum', 'labelNames' => [], 'labelValues' => $decodedLabelValues, 'value' => $histogramBuckets[$labelValues]['sum'], ]; } $histograms[] = new MetricFamilySamples($data); } return $histograms; } /** * @param array $metrics * @return array */ private function internalCollect(array $metrics): array { $result = []; foreach ($metrics as $metric) { $metaData = $metric['meta']; $data = [ 'name' => $metaData['name'], 'help' => $metaData['help'], 'type' => $metaData['type'], 'labelNames' => $metaData['labelNames'], ]; foreach ($metric['samples'] as $key => $value) { $parts = explode(':', $key); $labelValues = $parts[2]; $data['samples'][] = [ 'name' => $metaData['name'], 'labelNames' => [], 'labelValues' => $this->decodeLabelValues($labelValues), 'value' => $value, ]; } $this->sortSamples($data['samples']); $result[] = new MetricFamilySamples($data); } return $result; } /** * @param array $data * @return void */ public function updateHistogram(array $data): void { // Initialize the sum $metaKey = $this->metaKey($data); if (array_key_exists($metaKey, $this->histograms) === false) { $this->histograms[$metaKey] = [ 'meta' => $this->metaData($data), 'samples' => [], ]; } $sumKey = $this->histogramBucketValueKey($data, 'sum'); if (array_key_exists($sumKey, $this->histograms[$metaKey]['samples']) === false) { $this->histograms[$metaKey]['samples'][$sumKey] = 0; } $this->histograms[$metaKey]['samples'][$sumKey] += $data['value']; $bucketToIncrease = '+Inf'; foreach ($data['buckets'] as $bucket) { if ($data['value'] <= $bucket) { $bucketToIncrease = $bucket; break; } } $bucketKey = $this->histogramBucketValueKey($data, $bucketToIncrease); if (array_key_exists($bucketKey, $this->histograms[$metaKey]['samples']) === false) { $this->histograms[$metaKey]['samples'][$bucketKey] = 0; } $this->histograms[$metaKey]['samples'][$bucketKey] += 1; } /** * @param array $data */ public function updateGauge(array $data): void { $metaKey = $this->metaKey($data); $valueKey = $this->valueKey($data); if (array_key_exists($metaKey, $this->gauges) === false) { $this->gauges[$metaKey] = [ 'meta' => $this->metaData($data), 'samples' => [], ]; } if (array_key_exists($valueKey, $this->gauges[$metaKey]['samples']) === false) { $this->gauges[$metaKey]['samples'][$valueKey] = 0; } if ($data['command'] === Adapter::COMMAND_SET) { $this->gauges[$metaKey]['samples'][$valueKey] = $data['value']; } else { $this->gauges[$metaKey]['samples'][$valueKey] += $data['value']; } } /** * @param array $data */ public function updateCounter(array $data): void { $metaKey = $this->metaKey($data); $valueKey = $this->valueKey($data); if (array_key_exists($metaKey, $this->counters) === false) { $this->counters[$metaKey] = [ 'meta' => $this->metaData($data), 'samples' => [], ]; } if (array_key_exists($valueKey, $this->counters[$metaKey]['samples']) === false) { $this->counters[$metaKey]['samples'][$valueKey] = 0; } if ($data['command'] === Adapter::COMMAND_SET) { $this->counters[$metaKey]['samples'][$valueKey] = 0; } else { $this->counters[$metaKey]['samples'][$valueKey] += $data['value']; } } /** * @param array $data * @param string $bucket * @return string */ private function histogramBucketValueKey(array $data, $bucket): string { return implode(':', [ $data['type'], $data['name'], $this->encodeLabelValues($data['labelValues']), $bucket, ]); } /** * @param array $data * * @return string */ private function metaKey(array $data): string { return implode(':', [ $data['type'], $data['name'], 'meta' ]); } /** * @param array $data * * @return string */ private function valueKey(array $data): string { return implode(':', [ $data['type'], $data['name'], $this->encodeLabelValues($data['labelValues']), 'value' ]); } /** * @param array $data * * @return array */ private function metaData(array $data): array { $metricsMetaData = $data; unset($metricsMetaData['value']); unset($metricsMetaData['command']); unset($metricsMetaData['labelValues']); return $metricsMetaData; } /** * @param array $samples */ private function sortSamples(array &$samples): void { usort($samples, function ($a, $b) { return strcmp(implode("", $a['labelValues']), implode("", $b['labelValues'])); }); } /** * @param array $values * @return string * @throws RuntimeException */ private function encodeLabelValues(array $values): string { $json = json_encode($values); if (false === $json) { throw new RuntimeException(json_last_error_msg()); } return base64_encode($json); } /** * @param string $values * @return array * @throws RuntimeException */ private function decodeLabelValues($values): array { $json = base64_decode($values, true); if (false === $json) { throw new RuntimeException('Cannot base64 decode label values'); } $decodedValues = json_decode($json, true); if (false === $decodedValues) { throw new RuntimeException(json_last_error_msg()); } return $decodedValues; } } ================================================ FILE: src/Prometheus/Storage/Redis.php ================================================ '127.0.0.1', 'port' => 6379, 'timeout' => 0.1, 'read_timeout' => '10', 'persistent_connections' => false, 'password' => null, ]; /** * @var string */ private static $prefix = 'PROMETHEUS_'; /** * @var array */ private $options = []; /** * @var \Redis */ private $redis; /** * @var boolean */ private $connectionInitialized = false; /** * Redis constructor. * @param array $options */ public function __construct(array $options = []) { $this->options = array_merge(self::$defaultOptions, $options); $this->redis = new \Redis(); } public static function fromExistingConnection(\Redis $redis): self { if ($redis->isConnected() === false) { throw new StorageException('Connection to Redis server not established'); } $self = new self(); $self->connectionInitialized = true; $self->redis = $redis; return $self; } /** * @param array $options */ public static function setDefaultOptions(array $options): void { self::$defaultOptions = array_merge(self::$defaultOptions, $options); } /** * @param $prefix */ public static function setPrefix($prefix): void { self::$prefix = $prefix; } /** * @throws StorageException */ public function flushRedis(): void { $this->openConnection(); $this->redis->flushAll(); } /** * @return MetricFamilySamples[] * @throws StorageException */ public function collect(): array { $this->openConnection(); $metrics = $this->collectHistograms(); $metrics = array_merge($metrics, $this->collectGauges()); $metrics = array_merge($metrics, $this->collectCounters()); return array_map( function (array $metric) { return new MetricFamilySamples($metric); }, $metrics ); } /** * @throws StorageException */ private function openConnection(): void { if ($this->connectionInitialized === true) { return; } $connectionStatus = $this->connectToServer(); if ($connectionStatus === false) { throw new StorageException("Can't connect to Redis server", 0); } if ($this->options['password']) { $this->redis->auth($this->options['password']); } if (isset($this->options['database'])) { $this->redis->select($this->options['database']); } $this->redis->setOption(\Redis::OPT_READ_TIMEOUT, $this->options['read_timeout']); } /** * @return bool */ private function connectToServer(): bool { try { if ($this->options['persistent_connections']) { return $this->redis->pconnect( $this->options['host'], $this->options['port'], $this->options['timeout'] ); } return $this->redis->connect($this->options['host'], $this->options['port'], $this->options['timeout']); } catch (\RedisException $e) { return false; } } /** * @param array $data * @throws StorageException */ public function updateHistogram(array $data): void { $this->openConnection(); $bucketToIncrease = '+Inf'; foreach ($data['buckets'] as $bucket) { if ($data['value'] <= $bucket) { $bucketToIncrease = $bucket; break; } } $metaData = $data; unset($metaData['value']); unset($metaData['labelValues']); $this->redis->eval( <<toMetricKey($data), self::$prefix . Histogram::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX, json_encode(['b' => 'sum', 'labelValues' => $data['labelValues']]), json_encode(['b' => $bucketToIncrease, 'labelValues' => $data['labelValues']]), $data['value'], json_encode($metaData), ], 2 ); } /** * @param array $data * @throws StorageException */ public function updateGauge(array $data): void { $this->openConnection(); $metaData = $data; unset($metaData['value']); unset($metaData['labelValues']); unset($metaData['command']); $this->redis->eval( <<toMetricKey($data), self::$prefix . Gauge::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX, $this->getRedisCommand($data['command']), json_encode($data['labelValues']), $data['value'], json_encode($metaData), ], 2 ); } /** * @param array $data * @throws StorageException */ public function updateCounter(array $data): void { $this->openConnection(); $metaData = $data; unset($metaData['value']); unset($metaData['labelValues']); unset($metaData['command']); $this->redis->eval( <<toMetricKey($data), self::$prefix . Counter::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX, $this->getRedisCommand($data['command']), $data['value'], json_encode($data['labelValues']), json_encode($metaData), ], 2 ); } /** * @return array */ private function collectHistograms(): array { $keys = $this->redis->sMembers(self::$prefix . Histogram::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX); sort($keys); $histograms = []; foreach ($keys as $key) { $raw = $this->redis->hGetAll(str_replace($this->redis->_prefix(''), '', $key)); $histogram = json_decode($raw['__meta'], true); unset($raw['__meta']); $histogram['samples'] = []; // Add the Inf bucket so we can compute it later on $histogram['buckets'][] = '+Inf'; $allLabelValues = []; foreach (array_keys($raw) as $k) { $d = json_decode($k, true); if ($d['b'] == 'sum') { continue; } $allLabelValues[] = $d['labelValues']; } // We need set semantics. // This is the equivalent of array_unique but for arrays of arrays. $allLabelValues = array_map("unserialize", array_unique(array_map("serialize", $allLabelValues))); sort($allLabelValues); foreach ($allLabelValues as $labelValues) { // Fill up all buckets. // If the bucket doesn't exist fill in values from // the previous one. $acc = 0; foreach ($histogram['buckets'] as $bucket) { $bucketKey = json_encode(['b' => $bucket, 'labelValues' => $labelValues]); if (!isset($raw[$bucketKey])) { $histogram['samples'][] = [ 'name' => $histogram['name'] . '_bucket', 'labelNames' => ['le'], 'labelValues' => array_merge($labelValues, [$bucket]), 'value' => $acc, ]; } else { $acc += $raw[$bucketKey]; $histogram['samples'][] = [ 'name' => $histogram['name'] . '_bucket', 'labelNames' => ['le'], 'labelValues' => array_merge($labelValues, [$bucket]), 'value' => $acc, ]; } } // Add the count $histogram['samples'][] = [ 'name' => $histogram['name'] . '_count', 'labelNames' => [], 'labelValues' => $labelValues, 'value' => $acc, ]; // Add the sum $histogram['samples'][] = [ 'name' => $histogram['name'] . '_sum', 'labelNames' => [], 'labelValues' => $labelValues, 'value' => $raw[json_encode(['b' => 'sum', 'labelValues' => $labelValues])], ]; } $histograms[] = $histogram; } return $histograms; } /** * @return array */ private function collectGauges(): array { $keys = $this->redis->sMembers(self::$prefix . Gauge::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX); sort($keys); $gauges = []; foreach ($keys as $key) { $raw = $this->redis->hGetAll(str_replace($this->redis->_prefix(''), '', $key)); $gauge = json_decode($raw['__meta'], true); unset($raw['__meta']); $gauge['samples'] = []; foreach ($raw as $k => $value) { $gauge['samples'][] = [ 'name' => $gauge['name'], 'labelNames' => [], 'labelValues' => json_decode($k, true), 'value' => $value, ]; } usort($gauge['samples'], function ($a, $b) { return strcmp(implode("", $a['labelValues']), implode("", $b['labelValues'])); }); $gauges[] = $gauge; } return $gauges; } /** * @return array */ private function collectCounters(): array { $keys = $this->redis->sMembers(self::$prefix . Counter::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX); sort($keys); $counters = []; foreach ($keys as $key) { $raw = $this->redis->hGetAll(str_replace($this->redis->_prefix(''), '', $key)); $counter = json_decode($raw['__meta'], true); unset($raw['__meta']); $counter['samples'] = []; foreach ($raw as $k => $value) { $counter['samples'][] = [ 'name' => $counter['name'], 'labelNames' => [], 'labelValues' => json_decode($k, true), 'value' => $value, ]; } usort($counter['samples'], function ($a, $b) { return strcmp(implode("", $a['labelValues']), implode("", $b['labelValues'])); }); $counters[] = $counter; } return $counters; } /** * @param int $cmd * @return string */ private function getRedisCommand(int $cmd): string { switch ($cmd) { case Adapter::COMMAND_INCREMENT_INTEGER: return 'hIncrBy'; case Adapter::COMMAND_INCREMENT_FLOAT: return 'hIncrByFloat'; case Adapter::COMMAND_SET: return 'hSet'; default: throw new InvalidArgumentException("Unknown command"); } } /** * @param array $data * @return string */ private function toMetricKey(array $data): string { return implode(':', [self::$prefix, $data['type'], $data['name']]); } } ================================================ FILE: tests/Test/BlackBoxPushGatewayTest.php ================================================ registerCounter('test', 'some_counter', 'it increases', ['type']); $counter->incBy(6, ['blue']); $pushGateway = new PushGateway('pushgateway:9091'); $pushGateway->push($registry, 'my_job', ['instance' => 'foo']); $httpClient = new Client(); $metrics = $httpClient->get("http://pushgateway:9091/metrics")->getBody()->getContents(); $this->assertContains( '# HELP test_some_counter it increases # TYPE test_some_counter counter test_some_counter{instance="foo",job="my_job",type="blue"} 6', $metrics ); $pushGateway->delete('my_job', ['instance' => 'foo']); $httpClient = new Client(); $metrics = $httpClient->get("http://pushgateway:9091/metrics")->getBody()->getContents(); $this->assertNotContains( '# HELP test_some_counter it increases # TYPE test_some_counter counter test_some_counter{instance="foo",job="my_job",type="blue"} 6', $metrics ); } } ================================================ FILE: tests/Test/BlackBoxTest.php ================================================ adapter = getenv('ADAPTER'); $this->client = new Client(['base_uri' => 'http://nginx:80/']); $this->client->get('/examples/flush_adapter.php?adapter=' . $this->adapter); } /** * @test */ public function gaugesShouldBeOverwritten() { $start = microtime(true); $promises = [ $this->client->getAsync('/examples/some_gauge.php?c=0&adapter=' . $this->adapter), $this->client->getAsync('/examples/some_gauge.php?c=1&adapter=' . $this->adapter), $this->client->getAsync('/examples/some_gauge.php?c=2&adapter=' . $this->adapter), ]; Promise\settle($promises)->wait(); $end = microtime(true); echo "\ntime: " . ($end - $start) . "\n"; $metricsResult = $this->client->get('/examples/metrics.php?adapter=' . $this->adapter); $body = (string)$metricsResult->getBody(); echo "\nbody: " . $body . "\n"; $this->assertThat( $body, $this->logicalOr( $this->stringContains('test_some_gauge{type="blue"} 0'), $this->stringContains('test_some_gauge{type="blue"} 1'), $this->stringContains('test_some_gauge{type="blue"} 2') ) ); } /** * @test */ public function countersShouldIncrementAtomically() { $start = microtime(true); $promises = []; $sum = 0; for ($i = 0; $i < 1100; $i++) { $promises[] = $this->client->getAsync('/examples/some_counter.php?c=' . $i . '&adapter=' . $this->adapter); $sum += $i; } Promise\settle($promises)->wait(); $end = microtime(true); echo "\ntime: " . ($end - $start) . "\n"; $metricsResult = $this->client->get('/examples/metrics.php?adapter=' . $this->adapter); $body = (string)$metricsResult->getBody(); $this->assertThat($body, $this->stringContains('test_some_counter{type="blue"} ' . $sum)); } /** * @test */ public function histogramsShouldIncrementAtomically() { $start = microtime(true); $promises = [ $this->client->getAsync('/examples/some_histogram.php?c=0&adapter=' . $this->adapter), $this->client->getAsync('/examples/some_histogram.php?c=1&adapter=' . $this->adapter), $this->client->getAsync('/examples/some_histogram.php?c=2&adapter=' . $this->adapter), $this->client->getAsync('/examples/some_histogram.php?c=3&adapter=' . $this->adapter), $this->client->getAsync('/examples/some_histogram.php?c=4&adapter=' . $this->adapter), $this->client->getAsync('/examples/some_histogram.php?c=5&adapter=' . $this->adapter), $this->client->getAsync('/examples/some_histogram.php?c=6&adapter=' . $this->adapter), $this->client->getAsync('/examples/some_histogram.php?c=7&adapter=' . $this->adapter), $this->client->getAsync('/examples/some_histogram.php?c=8&adapter=' . $this->adapter), $this->client->getAsync('/examples/some_histogram.php?c=9&adapter=' . $this->adapter), ]; Promise\settle($promises)->wait(); $end = microtime(true); echo "\ntime: " . ($end - $start) . "\n"; $metricsResult = $this->client->get('/examples/metrics.php?adapter=' . $this->adapter); $body = (string)$metricsResult->getBody(); $this->assertThat($body, $this->stringContains(<<adapter = new APC(); $this->adapter->flushAPC(); } } ================================================ FILE: tests/Test/Prometheus/APC/CounterTest.php ================================================ adapter = new APC(); $this->adapter->flushAPC(); } } ================================================ FILE: tests/Test/Prometheus/APC/GaugeTest.php ================================================ adapter = new APC(); $this->adapter->flushAPC(); } } ================================================ FILE: tests/Test/Prometheus/APC/HistogramTest.php ================================================ adapter = new APC(); $this->adapter->flushAPC(); } } ================================================ FILE: tests/Test/Prometheus/AbstractCollectorRegistryTest.php ================================================ configureAdapter(); $this->renderer = new RenderTextFormat(); } /** * @test */ public function itShouldSaveGauges() { $registry = new CollectorRegistry($this->adapter); $g = $registry->registerGauge('test', 'some_metric', 'this is for testing', ['foo']); $g->set(35, ['bbb']); $g->set(35, ['ddd']); $g->set(35, ['aaa']); $g->set(35, ['ccc']); $registry = new CollectorRegistry($this->adapter); $this->assertThat( $this->renderer->render($registry->getMetricFamilySamples()), $this->equalTo( <<adapter); $metric = $registry->registerCounter('test', 'some_metric', 'this is for testing', ['foo', 'bar']); $metric->incBy(2, ['lalal', 'lululu']); $registry->getCounter('test', 'some_metric', ['foo', 'bar'])->inc(['lalal', 'lululu']); $registry->getCounter('test', 'some_metric', ['foo', 'bar'])->inc(['lalal', 'lvlvlv']); $registry = new CollectorRegistry($this->adapter); $this->assertThat( $this->renderer->render($registry->getMetricFamilySamples()), $this->equalTo( <<adapter); $metric = $registry->registerHistogram( 'test', 'some_metric', 'this is for testing', ['foo', 'bar'], [0.1, 1, 5, 10] ); $metric->observe(2, ['lalal', 'lululu']); $registry->getHistogram('test', 'some_metric', ['foo', 'bar'])->observe(7.1, ['lalal', 'lvlvlv']); $registry->getHistogram('test', 'some_metric', ['foo', 'bar'])->observe(13, ['lalal', 'lululu']); $registry->getHistogram('test', 'some_metric', ['foo', 'bar'])->observe(7.1, ['lalal', 'lululu']); $registry->getHistogram('test', 'some_metric', ['foo', 'bar'])->observe(7.1, ['gnaaha', 'hihihi']); $registry = new CollectorRegistry($this->adapter); $this->assertThat( $this->renderer->render($registry->getMetricFamilySamples()), $this->equalTo( <<adapter); $metric = $registry->registerHistogram('test', 'some_metric', 'this is for testing'); $metric->observe(2); $registry->getHistogram('test', 'some_metric')->observe(13); $registry->getHistogram('test', 'some_metric')->observe(7.1); $registry = new CollectorRegistry($this->adapter); $this->assertThat( $this->renderer->render($registry->getMetricFamilySamples()), $this->equalTo( <<adapter); $registry ->registerCounter('', 'some_quick_counter', 'just a quick measurement') ->inc() ; $this->assertThat( $this->renderer->render($registry->getMetricFamilySamples()), $this->equalTo( <<adapter); $registry->registerCounter('foo', 'metric', 'help'); $this->expectException(MetricsRegistrationException::class); $registry->registerCounter('foo', 'metric', 'help'); } /** * @test */ public function itShouldForbidRegisteringTheSameCounterWithDifferentLabels() { $registry = new CollectorRegistry($this->adapter); $registry->registerCounter('foo', 'metric', 'help', ["foo", "bar"]); $this->expectException(MetricsRegistrationException::class); $registry->registerCounter('foo', 'metric', 'help', ["spam", "eggs"]); } /** * @test */ public function itShouldForbidRegisteringTheSameHistogramTwice() { $registry = new CollectorRegistry($this->adapter); $registry->registerHistogram('foo', 'metric', 'help'); $this->expectException(MetricsRegistrationException::class); $registry->registerHistogram('foo', 'metric', 'help'); } /** * @test */ public function itShouldForbidRegisteringTheSameHistogramWithDifferentLabels() { $registry = new CollectorRegistry($this->adapter); $registry->registerCounter('foo', 'metric', 'help', ["foo", "bar"]); $this->expectException(MetricsRegistrationException::class); $registry->registerCounter('foo', 'metric', 'help', ["spam", "eggs"]); } /** * @test */ public function itShouldForbidRegisteringTheSameGaugeTwice() { $registry = new CollectorRegistry($this->adapter); $registry->registerGauge('foo', 'metric', 'help'); $this->expectException(MetricsRegistrationException::class); $registry->registerGauge('foo', 'metric', 'help'); } /** * @test */ public function itShouldForbidRegisteringTheSameGaugeWithDifferentLabels() { $registry = new CollectorRegistry($this->adapter); $registry->registerGauge('foo', 'metric', 'help', ["foo", "bar"]); $this->expectException(MetricsRegistrationException::class); $registry->registerGauge('foo', 'metric', 'help', ["spam", "eggs"]); } /** * @test */ public function itShouldThrowAnExceptionWhenGettingANonExistentMetric() { $registry = new CollectorRegistry($this->adapter); $this->expectException(MetricNotFoundException::class); $registry->getGauge("not_here", "go_away"); } /** * @test */ public function itShouldNotRegisterACounterTwice() { $registry = new CollectorRegistry($this->adapter); $counterA = $registry->getOrRegisterCounter("foo", "bar", "Help text"); $counterB = $registry->getOrRegisterCounter("foo", "bar", "Help text"); $this->assertSame($counterA, $counterB); } /** * @test */ public function itShouldNotRegisterAGaugeTwice() { $registry = new CollectorRegistry($this->adapter); $gaugeA = $registry->getOrRegisterGauge("foo", "bar", "Help text"); $gaugeB = $registry->getOrRegisterGauge("foo", "bar", "Help text"); $this->assertSame($gaugeA, $gaugeB); } /** * @test */ public function itShouldNotRegisterAHistogramTwice() { $registry = new CollectorRegistry($this->adapter); $histogramA = $registry->getOrRegisterHistogram("foo", "bar", "Help text"); $histogramB = $registry->getOrRegisterHistogram("foo", "bar", "Help text"); $this->assertSame($histogramA, $histogramB); } abstract public function configureAdapter(); } ================================================ FILE: tests/Test/Prometheus/AbstractCounterTest.php ================================================ configureAdapter(); } abstract public function configureAdapter(); /** * @test */ public function itShouldIncreaseWithLabels() { $counter = new Counter($this->adapter, 'test', 'some_metric', 'this is for testing', ['foo', 'bar']); $counter->inc(['lalal', 'lululu']); $counter->inc(['lalal', 'lululu']); $counter->inc(['lalal', 'lululu']); $this->assertThat( $this->adapter->collect(), $this->equalTo( [ new MetricFamilySamples( [ 'type' => Counter::TYPE, 'help' => 'this is for testing', 'name' => 'test_some_metric', 'labelNames' => ['foo', 'bar'], 'samples' => [ [ 'labelValues' => ['lalal', 'lululu'], 'value' => 3, 'name' => 'test_some_metric', 'labelNames' => [], ], ], ] ), ] ) ); } /** * @test */ public function itShouldIncreaseWithoutLabelWhenNoLabelsAreDefined() { $counter = new Counter($this->adapter, 'test', 'some_metric', 'this is for testing'); $counter->inc(); $this->assertThat( $this->adapter->collect(), $this->equalTo( [ new MetricFamilySamples( [ 'type' => Counter::TYPE, 'help' => 'this is for testing', 'name' => 'test_some_metric', 'labelNames' => [], 'samples' => [ [ 'labelValues' => [], 'value' => 1, 'name' => 'test_some_metric', 'labelNames' => [], ], ], ] ), ] ) ); } /** * @test */ public function itShouldIncreaseTheCounterByAnArbitraryInteger() { $counter = new Counter($this->adapter, 'test', 'some_metric', 'this is for testing', ['foo', 'bar']); $counter->inc(['lalal', 'lululu']); $counter->incBy(123, ['lalal', 'lululu']); $this->assertThat( $this->adapter->collect(), $this->equalTo( [ new MetricFamilySamples( [ 'type' => Counter::TYPE, 'help' => 'this is for testing', 'name' => 'test_some_metric', 'labelNames' => ['foo', 'bar'], 'samples' => [ [ 'labelValues' => ['lalal', 'lululu'], 'value' => 124, 'name' => 'test_some_metric', 'labelNames' => [], ], ], ] ), ] ) ); } /** * @test */ public function itShouldRejectInvalidMetricsNames() { $this->expectException(InvalidArgumentException::class); new Counter($this->adapter, 'test', 'some metric invalid metric', 'help'); } /** * @test */ public function itShouldRejectInvalidLabelNames() { $this->expectException(InvalidArgumentException::class); new Counter($this->adapter, 'test', 'some_metric', 'help', ['invalid label']); } /** * @test * @dataProvider labelValuesDataProvider * * @param mixed $value The label value */ public function isShouldAcceptAnySequenceOfBasicLatinCharactersForLabelValues($value) { $label = 'foo'; $histogram = new Counter($this->adapter, 'test', 'some_metric', 'help', [$label]); $histogram->inc([$value]); $metrics = $this->adapter->collect(); $this->assertIsArray($metrics); $this->assertCount(1, $metrics); $this->assertContainsOnlyInstancesOf(MetricFamilySamples::class, $metrics); $metric = reset($metrics); $samples = $metric->getSamples(); $this->assertContainsOnlyInstancesOf(Sample::class, $samples); foreach ($samples as $sample) { $labels = array_combine( array_merge($metric->getLabelNames(), $sample->getLabelNames()), $sample->getLabelValues() ); $this->assertEquals($value, $labels[$label]); } } /** * @return array * @see isShouldAcceptArbitraryLabelValues */ public function labelValuesDataProvider() { $cases = []; // Basic Latin // See https://en.wikipedia.org/wiki/List_of_Unicode_characters#Basic_Latin for ($i = 32; $i <= 121; $i++) { $cases['ASCII code ' . $i] = [chr($i)]; } return $cases; } } ================================================ FILE: tests/Test/Prometheus/AbstractGaugeTest.php ================================================ configureAdapter(); } abstract public function configureAdapter(); /** * @test */ public function itShouldAllowSetWithLabels() { $gauge = new Gauge($this->adapter, 'test', 'some_metric', 'this is for testing', ['foo', 'bar']); $gauge->set(123, ['lalal', 'lululu']); $this->assertThat( $this->adapter->collect(), $this->equalTo( [ new MetricFamilySamples( [ 'name' => 'test_some_metric', 'help' => 'this is for testing', 'type' => Gauge::TYPE, 'labelNames' => ['foo', 'bar'], 'samples' => [ [ 'name' => 'test_some_metric', 'labelNames' => [], 'labelValues' => ['lalal', 'lululu'], 'value' => 123, ], ], ] ), ] ) ); $this->assertThat($gauge->getHelp(), $this->equalTo('this is for testing')); $this->assertThat($gauge->getType(), $this->equalTo(Gauge::TYPE)); } /** * @test */ public function itShouldAllowSetWithoutLabelWhenNoLabelsAreDefined() { $gauge = new Gauge($this->adapter, 'test', 'some_metric', 'this is for testing'); $gauge->set(123); $this->assertThat( $this->adapter->collect(), $this->equalTo( [ new MetricFamilySamples( [ 'name' => 'test_some_metric', 'help' => 'this is for testing', 'type' => Gauge::TYPE, 'labelNames' => [], 'samples' => [ [ 'name' => 'test_some_metric', 'labelNames' => [], 'labelValues' => [], 'value' => 123, ], ], ] ), ] ) ); $this->assertThat($gauge->getHelp(), $this->equalTo('this is for testing')); $this->assertThat($gauge->getType(), $this->equalTo(Gauge::TYPE)); } /** * @test */ public function itShouldAllowSetWithAFloatValue() { $gauge = new Gauge($this->adapter, 'test', 'some_metric', 'this is for testing'); $gauge->set(123.5); $this->assertThat( $this->adapter->collect(), $this->equalTo( [ new MetricFamilySamples( [ 'name' => 'test_some_metric', 'help' => 'this is for testing', 'type' => Gauge::TYPE, 'labelNames' => [], 'samples' => [ [ 'name' => 'test_some_metric', 'labelNames' => [], 'labelValues' => [], 'value' => 123.5, ], ], ] ), ] ) ); $this->assertThat($gauge->getHelp(), $this->equalTo('this is for testing')); $this->assertThat($gauge->getType(), $this->equalTo(Gauge::TYPE)); } /** * @test */ public function itShouldIncrementAValue() { $gauge = new Gauge($this->adapter, 'test', 'some_metric', 'this is for testing', ['foo', 'bar']); $gauge->inc(['lalal', 'lululu']); $gauge->incBy(123, ['lalal', 'lululu']); $this->assertThat( $this->adapter->collect(), $this->equalTo( [ new MetricFamilySamples( [ 'name' => 'test_some_metric', 'help' => 'this is for testing', 'type' => Gauge::TYPE, 'labelNames' => ['foo', 'bar'], 'samples' => [ [ 'name' => 'test_some_metric', 'labelNames' => [], 'labelValues' => ['lalal', 'lululu'], 'value' => 124, ], ], ] ), ] ) ); } /** * @test */ public function itShouldIncrementWithFloatValue() { $gauge = new Gauge($this->adapter, 'test', 'some_metric', 'this is for testing', ['foo', 'bar']); $gauge->inc(['lalal', 'lululu']); $gauge->incBy(123.5, ['lalal', 'lululu']); $this->assertThat( $this->adapter->collect(), $this->equalTo( [ new MetricFamilySamples( [ 'name' => 'test_some_metric', 'help' => 'this is for testing', 'type' => Gauge::TYPE, 'labelNames' => ['foo', 'bar'], 'samples' => [ [ 'name' => 'test_some_metric', 'labelNames' => [], 'labelValues' => ['lalal', 'lululu'], 'value' => 124.5, ], ], ] ), ] ) ); } /** * @test */ public function itShouldDecrementAValue() { $gauge = new Gauge($this->adapter, 'test', 'some_metric', 'this is for testing', ['foo', 'bar']); $gauge->dec(['lalal', 'lululu']); $gauge->decBy(123, ['lalal', 'lululu']); $this->assertThat( $this->adapter->collect(), $this->equalTo( [ new MetricFamilySamples( [ 'name' => 'test_some_metric', 'help' => 'this is for testing', 'type' => Gauge::TYPE, 'labelNames' => ['foo', 'bar'], 'samples' => [ [ 'name' => 'test_some_metric', 'labelNames' => [], 'labelValues' => ['lalal', 'lululu'], 'value' => -124, ], ], ] ), ] ) ); } /** * @test */ public function itShouldDecrementWithFloatValue() { $gauge = new Gauge($this->adapter, 'test', 'some_metric', 'this is for testing', ['foo', 'bar']); $gauge->dec(['lalal', 'lululu']); $gauge->decBy(123, ['lalal', 'lululu']); $this->assertThat( $this->adapter->collect(), $this->equalTo( [ new MetricFamilySamples( [ 'name' => 'test_some_metric', 'help' => 'this is for testing', 'type' => Gauge::TYPE, 'labelNames' => ['foo', 'bar'], 'samples' => [ [ 'name' => 'test_some_metric', 'labelNames' => [], 'labelValues' => ['lalal', 'lululu'], 'value' => -124, ], ], ] ), ] ) ); } /** * @test */ public function itShouldOverwriteWhenSettingTwice() { $gauge = new Gauge($this->adapter, 'test', 'some_metric', 'this is for testing', ['foo', 'bar']); $gauge->set(123, ['lalal', 'lululu']); $gauge->set(321, ['lalal', 'lululu']); $this->assertThat( $this->adapter->collect(), $this->equalTo( [ new MetricFamilySamples( [ 'name' => 'test_some_metric', 'help' => 'this is for testing', 'type' => Gauge::TYPE, 'labelNames' => ['foo', 'bar'], 'samples' => [ [ 'name' => 'test_some_metric', 'labelNames' => [], 'labelValues' => ['lalal', 'lululu'], 'value' => 321, ], ], ] ), ] ) ); } /** * @test */ public function itShouldRejectInvalidMetricsNames() { $this->expectException(InvalidArgumentException::class); new Gauge($this->adapter, 'test', 'some metric invalid metric', 'help'); } /** * @test */ public function itShouldRejectInvalidLabelNames() { $this->expectException(InvalidArgumentException::class); new Gauge($this->adapter, 'test', 'some_metric', 'help', ['invalid label']); } /** * @test * @dataProvider labelValuesDataProvider * * @param mixed $value The label value */ public function isShouldAcceptAnySequenceOfBasicLatinCharactersForLabelValues($value) { $label = 'foo'; $histogram = new Gauge($this->adapter, 'test', 'some_metric', 'help', [$label]); $histogram->inc([$value]); $metrics = $this->adapter->collect(); $this->assertIsArray($metrics); $this->assertCount(1, $metrics); $this->assertContainsOnlyInstancesOf(MetricFamilySamples::class, $metrics); $metric = reset($metrics); $samples = $metric->getSamples(); $this->assertContainsOnlyInstancesOf(Sample::class, $samples); foreach ($samples as $sample) { $labels = array_combine( array_merge($metric->getLabelNames(), $sample->getLabelNames()), $sample->getLabelValues() ); $this->assertEquals($value, $labels[$label]); } } /** * @return array * @see isShouldAcceptArbitraryLabelValues */ public function labelValuesDataProvider() { $cases = []; // Basic Latin // See https://en.wikipedia.org/wiki/List_of_Unicode_characters#Basic_Latin for ($i = 32; $i <= 121; $i++) { $cases['ASCII code ' . $i] = [chr($i)]; } return $cases; } } ================================================ FILE: tests/Test/Prometheus/AbstractHistogramTest.php ================================================ configureAdapter(); } abstract public function configureAdapter(); /** * @test */ public function itShouldObserveWithLabels() { $histogram = new Histogram( $this->adapter, 'test', 'some_metric', 'this is for testing', ['foo', 'bar'], [100, 200, 300] ); $histogram->observe(123, ['lalal', 'lululu']); $histogram->observe(245, ['lalal', 'lululu']); $this->assertThat( $this->adapter->collect(), $this->equalTo( [ new MetricFamilySamples( [ 'name' => 'test_some_metric', 'help' => 'this is for testing', 'type' => Histogram::TYPE, 'labelNames' => ['foo', 'bar'], 'samples' => [ [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => ['lalal', 'lululu', 100], 'value' => 0, ], [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => ['lalal', 'lululu', 200], 'value' => 1, ], [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => ['lalal', 'lululu', 300], 'value' => 2, ], [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => ['lalal', 'lululu', '+Inf'], 'value' => 2, ], [ 'name' => 'test_some_metric_count', 'labelNames' => [], 'labelValues' => ['lalal', 'lululu'], 'value' => 2, ], [ 'name' => 'test_some_metric_sum', 'labelNames' => [], 'labelValues' => ['lalal', 'lululu'], 'value' => 368, ], ], ] ), ] ) ); } /** * @test */ public function itShouldObserveWithoutLabelWhenNoLabelsAreDefined() { $histogram = new Histogram( $this->adapter, 'test', 'some_metric', 'this is for testing', [], [100, 200, 300] ); $histogram->observe(245); $this->assertThat( $this->adapter->collect(), $this->equalTo( [ new MetricFamilySamples( [ 'name' => 'test_some_metric', 'help' => 'this is for testing', 'type' => Histogram::TYPE, 'labelNames' => [], 'samples' => [ [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => [100], 'value' => 0, ], [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => [200], 'value' => 0, ], [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => [300], 'value' => 1, ], [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => ['+Inf'], 'value' => 1, ], [ 'name' => 'test_some_metric_count', 'labelNames' => [], 'labelValues' => [], 'value' => 1, ], [ 'name' => 'test_some_metric_sum', 'labelNames' => [], 'labelValues' => [], 'value' => 245, ], ], ] ), ] ) ); } /** * @test */ public function itShouldObserveValuesOfTypeDouble() { $histogram = new Histogram( $this->adapter, 'test', 'some_metric', 'this is for testing', [], [0.1, 0.2, 0.3] ); $histogram->observe(0.11); $histogram->observe(0.3); $this->assertThat( $this->adapter->collect(), $this->equalTo( [ new MetricFamilySamples( [ 'name' => 'test_some_metric', 'help' => 'this is for testing', 'type' => Histogram::TYPE, 'labelNames' => [], 'samples' => [ [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => [0.1], 'value' => 0, ], [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => [0.2], 'value' => 1, ], [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => [0.3], 'value' => 2, ], [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => ['+Inf'], 'value' => 2, ], [ 'name' => 'test_some_metric_count', 'labelNames' => [], 'labelValues' => [], 'value' => 2, ], [ 'name' => 'test_some_metric_sum', 'labelNames' => [], 'labelValues' => [], 'value' => 0.41, ], ], ] ), ] ) ); } /** * @test */ public function itShouldProvideDefaultBuckets() { // .005, .01, .025, .05, .075, .1, .25, .5, .75, 1.0, 2.5, 5.0, 7.5, 10.0 $histogram = new Histogram( $this->adapter, 'test', 'some_metric', 'this is for testing', [] ); $histogram->observe(0.11); $histogram->observe(0.03); $this->assertThat( $this->adapter->collect(), $this->equalTo( [ new MetricFamilySamples( [ 'name' => 'test_some_metric', 'help' => 'this is for testing', 'type' => Histogram::TYPE, 'labelNames' => [], 'samples' => [ [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => [0.005], 'value' => 0, ], [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => [0.01], 'value' => 0, ], [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => [0.025], 'value' => 0, ], [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => [0.05], 'value' => 1, ], [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => [0.075], 'value' => 1, ], [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => [0.1], 'value' => 1, ], [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => [0.25], 'value' => 2, ], [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => [0.5], 'value' => 2, ], [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => [0.75], 'value' => 2, ], [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => [1.0], 'value' => 2, ], [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => [2.5], 'value' => 2, ], [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => [5], 'value' => 2, ], [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => [7.5], 'value' => 2, ], [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => [10], 'value' => 2, ], [ 'name' => 'test_some_metric_bucket', 'labelNames' => ['le'], 'labelValues' => ['+Inf'], 'value' => 2, ], [ 'name' => 'test_some_metric_count', 'labelNames' => [], 'labelValues' => [], 'value' => 2, ], [ 'name' => 'test_some_metric_sum', 'labelNames' => [], 'labelValues' => [], 'value' => 0.14, ], ], ] ), ] ) ); } /** * @test */ public function itShouldThrowAnExceptionWhenTheBucketSizesAreNotIncreasing() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Histogram buckets must be in increasing order'); new Histogram($this->adapter, 'test', 'some_metric', 'this is for testing', [], [1, 1]); } /** * @test */ public function itShouldThrowAnExceptionWhenThereIsLessThanOneBucket() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Histogram must have at least one bucket'); new Histogram($this->adapter, 'test', 'some_metric', 'this is for testing', [], []); } /** * @test */ public function itShouldThrowAnExceptionWhenThereIsALabelNamedLe() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Histogram cannot have a label named'); new Histogram($this->adapter, 'test', 'some_metric', 'this is for testing', ['le'], [1]); } /** * @test */ public function itShouldRejectInvalidMetricsNames() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Invalid metric name'); new Histogram($this->adapter, 'test', 'some invalid metric', 'help', [], [1]); } /** * @test */ public function itShouldRejectInvalidLabelNames() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Invalid label name'); new Histogram($this->adapter, 'test', 'some_metric', 'help', ['invalid label'], [1]); } /** * @test * @dataProvider labelValuesDataProvider * * @param mixed $value The label value */ public function isShouldAcceptAnySequenceOfBasicLatinCharactersForLabelValues($value) { $label = 'foo'; $histogram = new Histogram($this->adapter, 'test', 'some_metric', 'help', [$label], [1]); $histogram->observe(1, [$value]); $metrics = $this->adapter->collect(); $this->assertIsArray($metrics); $this->assertCount(1, $metrics); $this->assertContainsOnlyInstancesOf(MetricFamilySamples::class, $metrics); $metric = reset($metrics); $samples = $metric->getSamples(); $this->assertContainsOnlyInstancesOf(Sample::class, $samples); foreach ($samples as $sample) { $labels = array_combine( array_merge($metric->getLabelNames(), $sample->getLabelNames()), $sample->getLabelValues() ); $this->assertEquals($value, $labels[$label]); } } /** * @test */ public function itShouldBeAbleToGenerateExponentialBucketsGivenSpecificBounds() { $start = 0.05; $growthFactor = 1.5; $numberOfbuckets = 14; $generatedBuckets = Histogram::exponentialBuckets($start, $growthFactor, $numberOfbuckets); $expectedBuckets = [ 0.05, 0.075, 0.1125, 0.16875, 0.253125, 0.3796875, 0.56953125, 0.854296875, 1.2814453125, 1.92216796875, 2.883251953125, 4.3248779296875, 6.4873168945313, 9.7309753417969, ]; $this->assertEquals($generatedBuckets, $expectedBuckets); } /** * @return array * @see isShouldAcceptArbitraryLabelValues */ public function labelValuesDataProvider() { $cases = []; // Basic Latin // See https://en.wikipedia.org/wiki/List_of_Unicode_characters#Basic_Latin for ($i = 32; $i <= 121; $i++) { $cases['ASCII code ' . $i] = [chr($i)]; } return $cases; } } ================================================ FILE: tests/Test/Prometheus/InMemory/CollectorRegistryTest.php ================================================ adapter = new InMemory(); $this->adapter->flushMemory(); } } ================================================ FILE: tests/Test/Prometheus/InMemory/CounterTest.php ================================================ adapter = new InMemory(); $this->adapter->flushMemory(); } } ================================================ FILE: tests/Test/Prometheus/InMemory/GaugeTest.php ================================================ adapter = new InMemory(); $this->adapter->flushMemory(); } } ================================================ FILE: tests/Test/Prometheus/InMemory/HistogramTest.php ================================================ adapter = new InMemory(); $this->adapter->flushMemory(); } } ================================================ FILE: tests/Test/Prometheus/PushGatewayTest.php ================================================ createMock(CollectorRegistry::class); $mockedCollectorRegistry->method('getMetricFamilySamples')->with()->willReturn([ $this->createMock(MetricFamilySamples::class) ]); $mockHandler = new MockHandler([ new Response(200), new Response(202), ]); $handler = HandlerStack::create($mockHandler); $client = new Client(['handler' => $handler]); $pushGateway = new PushGateway('http://foo.bar', $client); $pushGateway->push($mockedCollectorRegistry, 'foo'); } /** * @test * * @doesNotPerformAnyAssertions */ public function invalidResponseShouldThrowRuntimeException(): void { $this->expectException(\RuntimeException::class); $mockedCollectorRegistry = $this->createMock(CollectorRegistry::class); $mockedCollectorRegistry->method('getMetricFamilySamples')->with()->willReturn([ $this->createMock(MetricFamilySamples::class) ]); $mockHandler = new MockHandler([ new Response(201), new Response(300), ]); $handler = HandlerStack::create($mockHandler); $client = new Client(['handler' => $handler]); $pushGateway = new PushGateway('http://foo.bar', $client); $pushGateway->push($mockedCollectorRegistry, 'foo'); } /** * @test */ public function clientGetsDefinedIfNotSpecified(): void { $this->expectException(\RuntimeException::class); $mockedCollectorRegistry = $this->createMock(CollectorRegistry::class); $mockedCollectorRegistry->method('getMetricFamilySamples')->with()->willReturn([ $this->createMock(MetricFamilySamples::class) ]); $pushGateway = new PushGateway('http://foo.bar'); $pushGateway->push($mockedCollectorRegistry, 'foo'); } } ================================================ FILE: tests/Test/Prometheus/Redis/CollectorRegistryTest.php ================================================ adapter = new Redis(['host' => REDIS_HOST]); $this->adapter->flushRedis(); } } ================================================ FILE: tests/Test/Prometheus/Redis/CounterTest.php ================================================ adapter = new Redis(['host' => REDIS_HOST]); $this->adapter->flushRedis(); } } ================================================ FILE: tests/Test/Prometheus/Redis/CounterWithPrefixTest.php ================================================ connect(REDIS_HOST); $connection->setOption(\Redis::OPT_PREFIX, 'prefix:'); $this->adapter = Redis::fromExistingConnection($connection); $this->adapter->flushRedis(); } } ================================================ FILE: tests/Test/Prometheus/Redis/GaugeTest.php ================================================ adapter = new Redis(['host' => REDIS_HOST]); $this->adapter->flushRedis(); } } ================================================ FILE: tests/Test/Prometheus/Redis/GaugeWithPrefixTest.php ================================================ connect(REDIS_HOST); $connection->setOption(\Redis::OPT_PREFIX, 'prefix:'); $this->adapter = Redis::fromExistingConnection($connection); $this->adapter->flushRedis(); } } ================================================ FILE: tests/Test/Prometheus/Redis/HistogramTest.php ================================================ adapter = new Redis(['host' => REDIS_HOST]); $this->adapter->flushRedis(); } } ================================================ FILE: tests/Test/Prometheus/Redis/HistogramWithPrefixTest.php ================================================ connect(REDIS_HOST); $connection->setOption(\Redis::OPT_PREFIX, 'prefix:'); $this->adapter = Redis::fromExistingConnection($connection); $this->adapter->flushRedis(); } } ================================================ FILE: tests/Test/Prometheus/Storage/RedisTest.php ================================================ '/dev/null']); $this->expectException(StorageException::class); $this->expectExceptionMessage("Can't connect to Redis server"); $redis->collect(); $redis->flushRedis(); } /** * @test */ public function itShouldThrowExceptionWhenInjectedRedisIsNotConnected() { $connection = new \Redis(); $this->expectException(StorageException::class); $this->expectExceptionMessage('Connection to Redis server not established'); Redis::fromExistingConnection($connection); } } ================================================ FILE: tests/bootstrap.php ================================================ add('Test\\Prometheus', __DIR__); define('REDIS_HOST', isset($_SERVER['REDIS_HOST']) ? $_SERVER['REDIS_HOST'] : '127.0.0.1');