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
[](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');